この記事では、Windows 11 上の WSL2(Ubuntu)環境において、Python で作成したテトリスを Docker コンテナ化し、Windows 側の X Window System(VcXsrv)を利用して GUI アプリケーションとして起動する手順を説明しています。
この記事をよむことで、Python で書かれたプログラムをDockerイメージとして起動する手順を理解できます。
前提条件
- Windows 11 に Docker Desktop がインストールされており、WSL2 と連携していること
- WSL2 上に Ubuntu がインストールされていること
- Windows 側に VcXsrv がインストール済みで、WSL からの X11 フォワーディングが許可されていること
作業環境の構築
Docker Desktopのインストール
WindowsにDocker Desktopをインストールする方法を参考。
WSL2
WSL2 (Windows Subsystem for Linux 2) は、Microsoft が提供する Windows の機能で、Windows 上で Linux 環境を直接実行できるようにするものです。
Docker Desktopをインストールする過程でこれらは自動的に設定されます。
(Settings>General>Use the WSL 2 based engine)
Windows Terminal
PowerShell や WSL(bash)など複数のシェルをタブで切り替えながら利用できるターミナルアプリケーションです。
Windows ターミナル の基本を参考にインストールします。
Ubuntu(Linux)
Windows Terminalを使って、WSL2環境にUbuntu(Linux)をインストールします。
WSL を使用して Windows に Linux をインストールする方法を参考。
Docker Desktopの設定が、Settings>Resources WSL integration>Enable integration with my default WSL distroがON、Enable integration with additional distrosのUbuntuのトグルがONの状態で利用します。
VcXsrv のインストール(Windows 側)
- VcXsrv の公式リポジトリからインストーラーをダウンロード
- インストーラーを実行し、推奨設定(Multiple windows、Start no client、Disable access control)でインストール
- インストール後、VcXsrv を起動
- 「xlaunch」を実行
- 「Multiple windows」「Start no client」を選択
- 「Extra settings」で「Clipboard」、「Native open gl」、「Disable access control」にチェックを入れて完了



これで Windows 側で X サーバーが待ち受ける状態になります。
【参考】
VcXsrvは、LinuxのX11プロトコルをWindowsのGDI/DirectXに変換します。
これにより、X11ウィンドウをWindowsのネイティブウィンドウとして表示できます。
詳細は、WSL上にXサーバをインストールしてGUIを実現する(VcXsrv編)を参考してください。
プロジェクトの作成と実行
ディレクトリの作成と移動
mkdir tetris-docker
cd tetris-docker
空きファイルの作成
touch tetris.py Dockerfile requirements.txt docker-compose.yml
ファイル内容の作成
テトリスのPythonコード(tetris.py)の作成
tetris.pyに、以下のテトリスゲームのコードを設定します。
import pygame
import random
import sys
# 初期化
pygame.init()
# 色の定義
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
ORANGE = (255, 165, 0)
PURPLE = (128, 0, 128)
CYAN = (0, 255, 255)
# ゲーム設定
GRID_WIDTH = 10
GRID_HEIGHT = 20
CELL_SIZE = 30
GRID_X_OFFSET = 50
GRID_Y_OFFSET = 50
SCREEN_WIDTH = GRID_WIDTH * CELL_SIZE + 2 * GRID_X_OFFSET + 200
SCREEN_HEIGHT = GRID_HEIGHT * CELL_SIZE + 2 * GRID_Y_OFFSET
# テトリミノの形状
TETROMINOES = [
# I
[['.....',
'..#..',
'..#..',
'..#..',
'..#..'],
['.....',
'.....',
'####.',
'.....',
'.....']],
# O
[['.....',
'.....',
'.##..',
'.##..',
'.....']],
# T
[['.....',
'.....',
'.#...',
'###..',
'.....'],
['.....',
'.....',
'.#...',
'.##..',
'.#...'],
['.....',
'.....',
'.....',
'###..',
'.#...'],
['.....',
'.....',
'.#...',
'##...',
'.#...']],
# S
[['.....',
'.....',
'.##..',
'##...',
'.....'],
['.....',
'.....',
'#....',
'##...',
'.#...']],
# Z
[['.....',
'.....',
'##...',
'.##..',
'.....'],
['.....',
'.....',
'.#...',
'##...',
'#....']],
# J
[['.....',
'.....',
'#....',
'###..',
'.....'],
['.....',
'.....',
'.##..',
'.#...',
'.#...'],
['.....',
'.....',
'.....',
'###..',
'..#..'],
['.....',
'.....',
'.#...',
'.#...',
'##...']],
# L
[['.....',
'.....',
'..#..',
'###..',
'.....'],
['.....',
'.....',
'.#...',
'.#...',
'.##..'],
['.....',
'.....',
'.....',
'###..',
'#....'],
['.....',
'.....',
'##...',
'.#...',
'.#...']]
]
TETROMINO_COLORS = [CYAN, YELLOW, PURPLE, GREEN, RED, BLUE, ORANGE]
class Tetris:
def __init__(self):
self.grid = [[0 for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
self.current_piece = self.new_piece()
self.next_piece = self.new_piece()
self.score = 0
self.level = 1
self.lines_cleared = 0
self.fall_time = 0
self.fall_speed = 500 # milliseconds
def new_piece(self):
piece_type = random.randint(0, len(TETROMINOES) - 1)
return {
'type': piece_type,
'rotation': 0,
'x': GRID_WIDTH // 2 - 2,
'y': 0,
'shape': TETROMINOES[piece_type][0],
'color': TETROMINO_COLORS[piece_type]
}
def valid_move(self, piece, dx=0, dy=0, rotation=None):
if rotation is None:
rotation = piece['rotation']
shape = TETROMINOES[piece['type']][rotation]
for y, row in enumerate(shape):
for x, cell in enumerate(row):
if cell == '#':
nx = piece['x'] + x + dx
ny = piece['y'] + y + dy
if nx < 0 or nx >= GRID_WIDTH or ny >= GRID_HEIGHT:
return False
if ny >= 0 and self.grid[ny][nx]:
return False
return True
def place_piece(self, piece):
shape = TETROMINOES[piece['type']][piece['rotation']]
for y, row in enumerate(shape):
for x, cell in enumerate(row):
if cell == '#':
nx = piece['x'] + x
ny = piece['y'] + y
if ny >= 0:
self.grid[ny][nx] = piece['color']
def clear_lines(self):
lines_to_clear = []
for y in range(GRID_HEIGHT):
if all(self.grid[y]):
lines_to_clear.append(y)
for y in lines_to_clear:
del self.grid[y]
self.grid.insert(0, [0 for _ in range(GRID_WIDTH)])
lines_cleared = len(lines_to_clear)
self.lines_cleared += lines_cleared
self.score += lines_cleared * 100 * self.level
self.level = self.lines_cleared // 10 + 1
self.fall_speed = max(50, 500 - (self.level - 1) * 50)
return lines_cleared > 0
def game_over(self):
return not self.valid_move(self.current_piece)
def update(self, dt):
self.fall_time += dt
if self.fall_time >= self.fall_speed:
if self.valid_move(self.current_piece, dy=1):
self.current_piece['y'] += 1
else:
self.place_piece(self.current_piece)
self.clear_lines()
self.current_piece = self.next_piece
self.next_piece = self.new_piece()
self.fall_time = 0
def move_piece(self, dx):
if self.valid_move(self.current_piece, dx=dx):
self.current_piece['x'] += dx
def rotate_piece(self):
num_rotations = len(TETROMINOES[self.current_piece['type']])
new_rotation = (self.current_piece['rotation'] + 1) % num_rotations
if self.valid_move(self.current_piece, rotation=new_rotation):
self.current_piece['rotation'] = new_rotation
self.current_piece['shape'] = TETROMINOES[self.current_piece['type']][new_rotation]
def drop_piece(self):
while self.valid_move(self.current_piece, dy=1):
self.current_piece['y'] += 1
self.score += 2
def draw_grid(screen, tetris):
# グリッドの背景
grid_surface = pygame.Surface((GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE))
grid_surface.fill(BLACK)
# 配置済みのブロックを描画
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if tetris.grid[y][x]:
pygame.draw.rect(grid_surface, tetris.grid[y][x],
(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE))
pygame.draw.rect(grid_surface, WHITE,
(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE), 1)
# 現在のピースを描画
piece = tetris.current_piece
shape = TETROMINOES[piece['type']][piece['rotation']]
for y, row in enumerate(shape):
for x, cell in enumerate(row):
if cell == '#':
px = (piece['x'] + x) * CELL_SIZE
py = (piece['y'] + y) * CELL_SIZE
if py >= 0:
pygame.draw.rect(grid_surface, piece['color'],
(px, py, CELL_SIZE, CELL_SIZE))
pygame.draw.rect(grid_surface, WHITE,
(px, py, CELL_SIZE, CELL_SIZE), 2)
# グリッドラインを描画
for x in range(GRID_WIDTH + 1):
pygame.draw.line(grid_surface, WHITE,
(x * CELL_SIZE, 0), (x * CELL_SIZE, GRID_HEIGHT * CELL_SIZE))
for y in range(GRID_HEIGHT + 1):
pygame.draw.line(grid_surface, WHITE,
(0, y * CELL_SIZE), (GRID_WIDTH * CELL_SIZE, y * CELL_SIZE))
screen.blit(grid_surface, (GRID_X_OFFSET, GRID_Y_OFFSET))
def draw_info(screen, tetris, font):
info_x = GRID_X_OFFSET + GRID_WIDTH * CELL_SIZE + 20
# スコア
score_text = font.render(f"Score: {tetris.score}", True, WHITE)
screen.blit(score_text, (info_x, 50))
# レベル
level_text = font.render(f"Level: {tetris.level}", True, WHITE)
screen.blit(level_text, (info_x, 100))
# ライン数
lines_text = font.render(f"Lines: {tetris.lines_cleared}", True, WHITE)
screen.blit(lines_text, (info_x, 150))
# 操作方法
controls = [
"Controls:",
"A/D - Move",
"S - Soft Drop",
"W - Rotate",
"Space - Hard Drop",
"Q - Quit"
]
for i, control in enumerate(controls):
control_text = font.render(control, True, WHITE)
screen.blit(control_text, (info_x, 250 + i * 30))
def main():
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Tetris")
clock = pygame.time.Clock()
font = pygame.font.Font(None, 24)
tetris = Tetris()
running = True
while running:
dt = clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_q:
running = False
elif event.key == pygame.K_a:
tetris.move_piece(-1)
elif event.key == pygame.K_d:
tetris.move_piece(1)
elif event.key == pygame.K_s:
if tetris.valid_move(tetris.current_piece, dy=1):
tetris.current_piece['y'] += 1
tetris.score += 1
elif event.key == pygame.K_w:
tetris.rotate_piece()
elif event.key == pygame.K_SPACE:
tetris.drop_piece()
if not tetris.game_over():
tetris.update(dt)
else:
# ゲームオーバー処理
game_over_text = font.render("GAME OVER! Press Q to quit", True, WHITE)
screen.blit(game_over_text, (50, SCREEN_HEIGHT // 2))
# 描画
screen.fill(BLACK)
draw_grid(screen, tetris)
draw_info(screen, tetris, font)
pygame.display.flip()
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()
Dockerfile の作成
Dockerfile を作成し、必要なシステムライブラリと Python パッケージをインストールします。
# Python 3.11 slim ベース
FROM python:3.11-slim
# 必要なライブラリをインストール
RUN apt-get update && apt-get install -y \
libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
libfreetype6-dev libportmidi-dev libjpeg-dev python3-dev python3-numpy \
libx11-dev libxext-dev libxrender-dev libice-dev libsm-dev \
libgl1-mesa-glx libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*
# 作業ディレクトリ
WORKDIR /app
# Python 依存関係をインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# アプリケーションコードをコピー
COPY tetris.py .
# 非 root ユーザーを作成
RUN useradd -m -u 1000 tetris_user
USER tetris_user
# X11 フォワーディング用設定
ENV DISPLAY=:0
# 起動コマンド
CMD ["python", "tetris.py"]
requirements.txt の作成
pygame==2.5.2
requirements.txtは、Pythonプロジェクトで使用する外部ライブラリ(依存関係)を記述するテキストファイルです。
requirements.txtは以下の理由で必要になります。
再現性: 誰でも同じ環境でアプリケーションを実行可能
効率性: Dockerビルドの高速化
保守性: 依存関係の管理と更新が容易
協業: チーム開発での環境統一
デプロイ: 本番環境での確実な動作保証
Docker Composeファイルの作成
Docker Composeファイル(docker-compose.yml)は、複数のDockerコンテナを定義、設定、実行するプロセスを簡素化するためのファイルです。
docker compose upコマンド一つで、定義されたサービスをに起動でき、docker compose downコマンドで、すべてのサービスと関連するリソースを停止・削除できます。
services:
tetris:
build: .
container_name: tetris_game
environment:
- DISPLAY=${DISPLAY}
volumes:
- /tmp/.X11-unix:/tmp/.X11-unix:rw
network_mode: host
stdin_open: true
tty: true
environment: - DISPLAY=${DISPLAY}
DISPLAY: Xサーバー(グラフィカルな表示を管理するシステム)のアドレスを指定する環境変数です。
=${DISPLAY} とすることで、PCの環境変数 DISPLAY の値をそのままコンテナに渡しています。
これにより、コンテナ内のアプリケーションがホストマシンのディスプレイにグラフィカルなウィンドウを表示できるようになります。
volumes: - /tmp/.X11-unix:/tmp/.X11-unix:rw
これはボリュームマウントの設定です。
ホストマシンの特定のディレクトリをコンテナ内のディレクトリにマウントし、両者間でファイルを共有できるようにします。
/tmp/.X11-unix:
ホストマシンのディレクトリです。Xサーバーとの通信に必要なソケットファイルが置かれています。
/tmp/.X11-unix:
コンテナ内のディレクトリです。ホストのX11ソケットをここにマウントすることで、コンテナ内のアプリケーションがホストのXサーバーと通信できるようになります。
:rw:
マウントされたボリュームを読み書き可能(read-write)にします。
network_mode: host
この設定は、コンテナがホストのネットワークスタックを直接使用するように指定します。
通常、Dockerコンテナは独自の分離されたネットワークネームスペースを持ちますが、hostモードを使うと、コンテナはホストマシンと同じIPアドレスとネットワークインターフェースを共有します。
stdin_open: true
コンテナの標準入力 (stdin) を開いたままにします。
これにより、コンテナがバックグラウンドで実行されていても、コマンドラインから入力を受け付けることができます。
tty: true
コンテナに仮想TTY (擬似端末) を割り当てます。
これにより、コンテナの出力が整形され、色付きの表示やカーソル移動などが正しく機能するようになります。
stdin_open: true と組み合わせることで、対話型シェルやグラフィカルなアプリケーションが正しく動作できます。
ターミナル操作
ここからはUbuntuのターミナルでの作業です。
Ubuntuのターミナルを開いて、以下のコマンドを実行します。
X11の権限を設定
xhost +local:docker
xhostコマンド
xhostは、X Window Systemのアクセス制御を管理するコマンドです。
どのクライアント(アプリケーション)がXサーバーに接続して画面に描画できるかを制御します。
ここでは、local:docker: ローカルのdockerユーザー/プロセスにアクセスの許可を追加しています。
xhost: アクセス制御コマンド
+: 許可を追加(-は許可を削除)
local:docker: ローカルのdockerユーザー/プロセス
# bash画面
$ xhost +local:docker
non-network local connections being added to access control list
Docker イメージのビルド
docker build -t tetris-game .
docker build:
Dockerfileに書かれた命令を順番に実行してイメージを構築します。
-t tetris-game:
「タグ」オプションです。-t は --tag の省略形です。
tetris-game が、これから構築するDockerイメージに付ける名前(リポジトリ名とタグ)です。
. (ドット):
現在のディレクトリにある Dockerfileを使用して、Dockerイメージを構築します。
# bash画面
$ docker build -t tetris-game .
[+] Building 56.7s (13/13) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 882B 0.0s
=> [internal] load metadata for docker.io/library/python:3.11-slim 1.8s
=> [auth] library/python:pull token for registry-1.docker.io 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> CACHED [1/7] FROM docker.io/library/python:3.11-slim@sha256:9e1912aab0a30bbd9488eb79063f68f42a68a 0.0s
=> => resolve docker.io/library/python:3.11-slim@sha256:9e1912aab0a30bbd9488eb79063f68f42a68ab0946cb 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 10.13kB 0.0s
=> [2/7] RUN apt-get update && apt-get install -y libsdl2-dev libsdl2-image-dev libsdl2-mixer-d 30.5s
=> [3/7] WORKDIR /app 0.1s
=> [4/7] COPY requirements.txt . 0.0s
=> [5/7] RUN pip install --no-cache-dir -r requirements.txt 4.6s
=> [6/7] COPY tetris.py . 0.0s
=> [7/7] RUN useradd -m -u 1000 tetris_user 0.3s
=> exporting to image 19.2s
=> => exporting layers 16.2s
=> => exporting manifest sha256:bae0d6f70c36a0495af0b94a738550787d4f40cf7f1e3edc2e6274ac94c22e70 0.0s
=> => exporting config sha256:46563212047049867f218b53f545ab6d1d8ef1dc39565ef461cd91f20a1eb737 0.0s
=> => exporting attestation manifest sha256:cbb0e5a804a4a83c2dbd937d997fcaa9df431edfa774bd6e7438ae40 0.0s
=> => exporting manifest list sha256:0ce114fc0beab680aeb42cbc0b3f490b1d96cc05e857088b7de34697878cbd3 0.0s
=> => naming to docker.io/library/tetris-game:latest 0.0s
=> => unpacking to docker.io/library/tetris-game:latest
#イメージの確認
$ docker images tetris-game
REPOSITORY TAG IMAGE ID CREATED SIZE
tetris-game latest 0ce114fc0bea 15 minutes ago 1.06GB
コンテナ起動(テトリスのウィンドウ表示)
docker-compose up
Docker Composeの動作
1. docker-compose.ymlを読み込み
2. 必要なイメージが存在するかチェック
├─ イメージが存在する → そのイメージを使用
└─ イメージが存在しない → ビルドを実行
3. コンテナを起動
# bash画面
#コンテナ起動
$ docker-compose up
Compose can now delegate builds to bake for better performance.
To do so, set COMPOSE_BAKE=true.
[+] Building 1.7s (14/14) FINISHED docker:default
=> [tetris internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 882B 0.0s
=> [tetris internal] load metadata for docker.io/library/python:3.11-slim 1.5s
=> [tetris auth] library/python:pull token for registry-1.docker.io 0.0s
=> [tetris internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [tetris 1/7] FROM docker.io/library/python:3.11-slim@sha256:9e1912aab0a30bbd9488eb79063f68f42a68a 0.0s
=> => resolve docker.io/library/python:3.11-slim@sha256:9e1912aab0a30bbd9488eb79063f68f42a68ab0946cb 0.0s
=> [tetris internal] load build context 0.0s
=> => transferring context: 66B 0.0s
=> CACHED [tetris 2/7] RUN apt-get update && apt-get install -y libsdl2-dev libsdl2-image-dev li 0.0s
=> CACHED [tetris 3/7] WORKDIR /app 0.0s
=> CACHED [tetris 4/7] COPY requirements.txt . 0.0s
=> CACHED [tetris 5/7] RUN pip install --no-cache-dir -r requirements.txt 0.0s
=> CACHED [tetris 6/7] COPY tetris.py . 0.0s
=> CACHED [tetris 7/7] RUN useradd -m -u 1000 tetris_user 0.0s
=> [tetris] exporting to image 0.1s
=> => exporting layers 0.0s
=> => exporting manifest sha256:c173e74549e966856c47f8b435aa9fef4b183ba663a700971d212eee01b03d26 0.0s
=> => exporting config sha256:319924444349567d82170c8f1acab55fc7e06d58f6708ecb58a6eedfdec6ddd2 0.0s
=> => exporting attestation manifest sha256:3354bd8a9650b7d30d2c8a55cdee2baf55118bdd6c8515f3c5eba0da 0.0s
=> => exporting manifest list sha256:c88efe26e6308bcc5bd713ad69fe443e4b26d3efbbff7f54ba5702cfcaab28f 0.0s
=> => naming to docker.io/library/tetris-docker-tetris:latest 0.0s
=> => unpacking to docker.io/library/tetris-docker-tetris:latest 0.0s
=> [tetris] resolving provenance for metadata file 0.0s
[+] Running 2/2
✔ tetris Built 0.0s
✔ Container tetris_game Created 0.1s
Attaching to tetris_game
tetris_game | pygame 2.5.2 (SDL 2.28.2, Python 3.11.13)
tetris_game | Hello from the pygame community. https://www.pygame.org/contribute.html
tetris_game | ALSA lib confmisc.c:855:(parse_card) cannot find card '0'
tetris_game | ALSA lib conf.c:5180:(_snd_config_evaluate) function snd_func_card_inum returned error: No such file or directory
tetris_game | ALSA lib confmisc.c:422:(snd_func_concat) error evaluating strings
tetris_game | ALSA lib conf.c:5180:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
tetris_game | ALSA lib confmisc.c:1334:(snd_func_refer) error evaluating name
tetris_game | ALSA lib conf.c:5180:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
tetris_game | ALSA lib conf.c:5703:(snd_config_expand) Evaluate error: No such file or directory
tetris_game | ALSA lib pcm.c:2666:(snd_pcm_open_noupdate) Unknown PCM default
tetris_game exited with code 0
#コンテナの起動状況確認
~/tetris-docker$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8f66ffb86c99 tetris-docker-tetris "python tetris.py" 51 seconds ago Exited (0) 17 seconds ago tetris_game
正常に起動すると、Windows 上にテトリスのウィンドウが表示され、キーボード操作でプレイできます。

コンテナの停止と削除
docker compose down
docker compose down は、docker compose up で起動したDocker Composeアプリケーションに関連するすべてのリソースを停止し、削除します。
# bash画面
#コンテナの停止と削除
$ docker compose down
[+] Running 1/1
✔ Container tetris_game Removed 0.0s
#コンテナ削除確認
~/tetris-docker$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

