Dockerfile — 10 типовых образов

Приветствую! Здесь вы наверняка найдете, что ищете. Примеры в лаборатории рассчитаны на то, что мы разбираем что-то конкретное.

Текущая статья посвящена типовым Dockerfile (Node, Python, Go, React+Nginx, Spring Boot, PHP, .NET) — построчный разбор и проверка docker build.

Поэтому за теорией по текущей теме вам — в энциклопедию. Если ещё не погружались, то маршрут прост:

  1. Основы
  2. Система и сеть
  3. Данные и разметка
  4. Код и разработка
  5. Языки
  6. Искусственный интеллект
  7. Проект
  8. Инфраструктура и безопасность
  9. Спин-офф

Обязательно пройдитесь.

А теперь приступим к нашему предмету.

Теория и соседние материалы

Полный справочник инструкций — глава Dockerfile.

Команды CLI — Docker.

Несколько контейнеров — Compose.

Nginx после сборки фронта — конфиги nginx.


Три слова — образ, контейнер, слой

Термин Простыми словами Аналогия
Dockerfile Текстовый рецепт «как собрать программу в коробку» Рецепт торта
Образ (image) Готовая «коробка» с ОС, файлами и командой запуска Замороженный торт из магазина
Контейнер Запущенный экземпляр образа (процесс) Торт на столе, который едят
Слой (layer) Каждая строка RUN / COPY добавляет шаг; Docker кэширует неизменённые шаги Слои в торте: пока не меняете крем, нижние слои не перепекаете
  Dockerfile          docker build           docker run
  ──────────          ────────────           ──────────
  FROM node..   →    образ my-api:1    →    контейнер (процесс node)
  COPY ..
  RUN npm ci
  CMD ["node", ..]

Контекст сборки — папка, которую вы указываете точкой в конце: docker build -t имя .
В эту папку Docker смотрит при COPY. Лишнее отсекает .dockerignore.

Поиграйте с порядком инструкций в симуляторе выше — затем разберите пример №3 Node.js: там видно, зачем сначала копируют package-lock.json, потом исходники.


Шпаргалка инструкций Dockerfile

Инструкция Когда выполняется Зачем нужна
FROM Начало сборки / новая стадия «На чём строим» — alpine, node, python…
WORKDIR При сборке и в контейнере Текущая папка для COPY и RUN
COPY Сборка Скопировать файлы с вашего ПК в образ
RUN Сборка Установить пакеты, собрать проект
ENV Сборка + контейнер Переменные среды (NODE_ENV, пути)
EXPOSE Документация Какой порт слушает приложение внутри
USER Сборка + контейнер От какого пользователя идёт процесс
CMD / ENTRYPOINT Только при docker run Что запустить, когда контейнер стартует
HEALTHCHECK Во время работы контейнера Docker периодически проверяет «жив ли сервис»

Важно: EXPOSE 3000 не открывает порт на вашем ноутбуке. Чтобы зайти из браузера, нужен docker run -p 8080:3000 (слева — ваш ПК, справа — порт в контейнере).


Сборка и запуск

  1. Создайте папку проекта (например my-api/).
  2. Положите Dockerfile и .dockerignore в корень.
  3. Соберите образ (имя и тег — любые):
docker build -t myapp:1 .
  1. Запустите контейнер:
docker run --rm -p 8080:3000 myapp:1
Часть команды Смысл
docker build Прочитать Dockerfile и собрать образ
-t myapp:1 Имя образа myapp, тег 1 (версия)
. Контекст — текущая папка
docker run Создать контейнер из образа и запустить
--rm Удалить контейнер после остановки
-p 8080:3000 localhost:8080 на ПК → порт 3000 в контейнере
Docker Desktop на Windows

Перед docker build Docker Desktop должен быть Running. Ошибка Cannot connect to the Docker daemon — демон не запущен, а не опечатка в Dockerfile.


Общий .dockerignore

Создайте файл .dockerignore рядом с Dockerfile:

.git
.gitignore
.env
*.log
node_modules
__pycache__
.venv
dist
build
target
coverage
.idea
.vscode
README.md

Разбор по строкам:

Строка Зачем
.git История Git не нужна в образе; сборка быстрее
.env Пароли и ключи нельзя запекать в слои образа
node_modules Зависимости ставят npm ci внутри RUN, а не копируют с Windows/macOS
dist / target Собранные файлы с хоста могут быть от другой ОС
README.md Документация в runtime не нужна

Без .dockerignore команда COPY . . может затянуть гигабайты и сломать кэш.


Оглавление — 10 образов

Сценарий Порт
1 Проверка Docker
2 Статический сайт 80
3 Node.js API 3000
4 Python API 5000
5 Go-сервис 8080
6 React/Vue + nginx 80
7 Spring Boot 8080
8 PHP-сайт 80
9 .NET API 8080
10 Миграции / seed

1. Минимальный образ — «Привет, Docker»

Задача: убедиться, что Docker установлен и команды build / run работают.

Смысл простыми словами: вы не пишете приложение — вы проверяете цепочку «рецепт → образ → контейнер». Три строки Dockerfile достаточно для первой проверки Docker.

Структура:

hello/
├── Dockerfile
└── .dockerignore

Dockerfile:

FROM alpine:3.19

RUN echo "Образ собран успешно"

CMD ["echo", "Привет из контейнера!"]

Разбор по строкам:

Строка Что делает Docker Зачем так
FROM alpine:3.19 Берёт готовый базовый образ с Docker Hub (~7 МБ) Минимальная «операционка» для учебы; тег 3.19 фиксирует версию
RUN echo "Образ собран…" Во время сборки выполняет команду и создаёт новый слой Показывает разницу: RUN = при build, не при run
CMD ["echo", "Привет…"] Команда по умолчанию при docker run Exec-форма ["программа", "арг1"] — без оболочки /bin/sh

Что происходит при docker build -t hello:1 .:

  1. Docker скачивает alpine:3.19, если его ещё нет локально.
  2. Создаёт временный контейнер, выполняет echo → фиксирует слой.
  3. Записывает в образ метаданные CMD.
  4. Помечает результат тегом hello:1.

Сборка и проверка:

docker build -t hello:1 .
docker run --rm hello:1
Шаг Ожидаемый результат
build В конце Successfully tagged hello:1
run В терминале строка Привет из контейнера!
docker ps после run Пусто — контейнер уже завершился

Если не работает:

Ошибка Причина Что сделать
Cannot connect to the Docker daemon Docker не запущен Запустить Docker Desktop
unable to prepare context Нет прав на папку Открыть терминал в каталоге hello/
Пустой вывод Старая версия Docker Обновить Docker Desktop

2. Статический сайт на nginx

Задача: отдать HTML/CSS из папки public/ без Node, Python и без npm run build.

Смысл простыми словами: образ — это nginx + ваши файлы. Браузер запрашивает страницу → nginx читает файл с диска внутри контейнера → отдаёт HTML. Подходит для лендинга, учебного сайта, отчёта по вебу.

Структура:

static-site/
├── public/
│   └── index.html
├── Dockerfile
└── .dockerignore

public/index.html:

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <title>Статика в Docker</title>
</head>
<body>
  <h1>Сайт из Docker</h1>
  <p>Файл лежит в public/index.html и копируется в образ.</p>
</body>
</html>

Dockerfile:

FROM nginx:1.27-alpine

COPY public/ /usr/share/nginx/html/

EXPOSE 80

HEALTHCHECK CMD wget -qO- http://127.0.0.1/ || exit 1

Разбор по строкам:

Строка Что делает Зачем
FROM nginx:1.27-alpine Базовый образ с уже установленным nginx Не ставим nginx вручную через apt
COPY public/ /usr/share/nginx/html/ Копирует содержимое public/ в стандартную папку статики nginx URL / → файл index.html
EXPOSE 80 Пишет в метаданные «сервис слушает 80» Напоминание; порт на хост задаёт -p
HEALTHCHECK … wget … Раз в 30 с дергает главную страницу Статус healthy в docker ps; в учебе можно убрать

Что происходит при docker build:

  1. Слой nginx (из Hub).
  2. Новый слой — ваши HTML-файлы поверх /usr/share/nginx/html/.
  3. Образ готов; nginx не запущен до docker run.

Сборка и проверка:

docker build -t static:1 .
docker run --rm -p 8080:80 static:1
Действие Результат
Открыть http://localhost:8080 Заголовок «Сайт из Docker»
docker run без -p Сайт не откроется с ПК — порт не проброшен

Разбор -p 8080:80:

Число Где
8080 Порт на вашем компьютере (localhost)
80 Порт внутри контейнера, где слушает nginx

Тот же nginx одной строкой в Compose — стек №1.

Если не работает:

Симптом Причина
403 Forbidden Нет index.html или неверный путь COPY
Пустая страница Welcome to nginx COPY не туда — проверьте public/
Connection refused Забыли -p или занят порт 8080 — смените на -p 8888:80

3. Node.js API (Express / Fastify)

Задача: упаковать REST API в образ для production: без dev-зависимостей, с кэшем npm и не от root.

Смысл простыми словами: две стадии сборки. На первой ставят node_modules, на второй — только готовые файлы и зависимости для запуска. Компиляторы и eslint в финальный образ не попадают. Это самый частый запрос среди студентов на fullstack-курсах.

Структура:

my-api/
├── package.json
├── package-lock.json
├── src/
│   └── server.js
├── Dockerfile
└── .dockerignore

Минимальный src/server.js (чтобы пример завёлся):

Разбор по строкам:

Строка Что делает Зачем
FROM node:20-alpine AS deps Стадия 1 — только установка зависимостей Имя deps для COPY --from=deps
WORKDIR /app Дальше все пути относительно /app Как cd /app в терминале
COPY package.json package-lock.json ./ Копирует только манифесты При правке server.js слой npm ci берётся из кэша
RUN npm ci --omit=dev Ставит пакеты строго по lock-файлу ci = как в CI; --omit=dev без jest/eslint
FROM node:20-alpine AS runner Стадия 2 — чистый runtime В финале нет мусора стадии deps
ENV NODE_ENV=production Переменная для Node и библиотек Меньше отладочного режима
COPY --from=deps … node_modules Забирает папку из другой стадии Ключ multi-stage
COPY src ./src Исходники после зависимостей Правка кода не перезапускает npm ci
adduser + USER app Процесс не root Требование безопасности в отчётах
EXPOSE 3000 Документирует порт API Связка с -p …:3000
HEALTHCHECK … /health Проверка эндпоинта из server.js Без /health контейнер станет unhealthy
CMD ["node", "src/server.js"] Запуск при docker run Exec-форма — корректные сигналы остановки

Что происходит при docker build:

  1. Стадия deps: слои COPY package*RUN npm ci (долго только при смене lock-файла).
  2. Стадия runner: копирование node_modules из deps, затем src.
  3. Слой USER app — все последующие команды от непривилегированного пользователя.
  4. В финальный образ не входят исходники стадии deps, кроме node_modules.

Сборка и проверка:

docker build -t my-api:1 .
docker run --rm -p 3000:3000 my-api:1

В другом терминале:

curl http://localhost:3000/health
curl http://localhost:3000/
Команда Ожидаемый ответ
/health ok
/ Node API в Docker работает

Если не работает:

Симптом Причина Решение
curl с хоста пустой / timeout Сервер слушает 127.0.0.1 В коде listen(3000, '0.0.0.0')
npm ci падает Нет package-lock.json Выполнить npm install локально и закоммитить lock
EACCES при старте Права на файлы Перед USER добавить chown -R app:app /app
unhealthy Нет /health Добавить маршрут или убрать HEALTHCHECK

Манифесты npm — Манифесты зависимостей. API + Postgres в Compose — стек app + db.


4. Python (Flask / FastAPI)

Задача: запустить веб-приложение Python через gunicorn (production), а не через flask run.

Смысл простыми словами: образ содержит интерпретатор Python, установленные pip-пакеты и ваш app.py. При docker run стартует gunicorn — он принимает HTTP и передаёт запросы во Flask. Порядок COPY requirements.txtpip installCOPY . . экономит время при каждой правке кода.

Структура:

flask-app/
├── app.py
├── requirements.txt
├── Dockerfile
└── .dockerignore

app.py (минимум):

from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "Flask в Docker\n"

@app.route("/health")
def health():
    return "ok"

requirements.txt:

flask==3.0.0
gunicorn==21.2.0

Dockerfile:

Разбор по строкам:

Строка Смысл
python:3.12-slim Официальный образ Python; slim меньше, чем полный
PYTHONDONTWRITEBYTECODE=1 Не создавать .pyc в образе
PYTHONUNBUFFERED=1 print и логи сразу в docker logs
APP_HOME=/app Переменная для пути; удобно менять одно место
WORKDIR $APP_HOME Рабочая директория /app
COPY requirements.txt + RUN pip install Слой зависимостей отдельно от кода
COPY . . Весь проект после pip
adduser / chown / USER appuser Файлы принадлежат пользователю, не root
gunicorn --bind 0.0.0.0:5000 Слушать снаружи контейнера
--workers 2 Два процесса — для учебы достаточно
app:app Модуль app.py, объект Flask app

Таблица кэша (как в отчёте по Docker):

Изменили файл Что пересобирается
Только app.py Слои начиная с COPY . .
requirements.txt pip install и всё ниже
Dockerfile Часто вся сборка

Сборка и проверка:

docker build -t flask-app:1 .
docker run --rm -p 5000:5000 flask-app:1
curl http://localhost:5000/
curl http://localhost:5000/health

FastAPI — замените зависимости и CMD:

fastapi==0.110.0
uvicorn[standard]==0.27.0
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "5000"]

Если не работает:

Ошибка Причина
ModuleNotFoundError: flask Нет строки в requirements.txt
Failed to find attribute app В app.py объект должен называться app
Порт занят docker run -p 5001:5000

5. Go-сервис (multi-stage)

Задача: собрать один бинарник и положить его в крошечный образ без Go SDK.

Смысл простыми словами: на стадии builder компилятор Go превращает исходники в файл /server. В финальный образ копируется только этот файл — ни go.mod, ни исходников, ни компилятора. Образ на диске часто 15–25 МБ вместо сотен.

Структура:

go-service/
├── go.mod
├── go.sum
├── cmd/
│   └── server/
│       └── main.go
└── Dockerfile

Минимальный cmd/server/main.go:

Разбор по строкам:

Строка Смысл
golang:1.22-alpine AS builder Образ с компилятором Go
COPY go.mod go.sum + go mod download Кэш модулей до копирования исходников
CGO_ENABLED=0 Бинарник без привязки к C-библиотекам хоста
GOOS=linux Целевая ОС внутри контейнера
-ldflags="-s -w" Убрать отладочные символы — файл меньше
-o /server ./cmd/server Имя бинарника и пакет с main
FROM alpine:3.19 Финал — только Linux + ваш бинарник
COPY --from=builder /server /server Единственный артефакт из стадии сборки
CMD ["/server"] Запуск бинарника как PID 1

Сборка и проверка:

docker build -t go-svc:1 .
docker run --rm -p 8080:8080 go-svc:1
curl http://localhost:8080/health

Если не работает:

Ошибка Решение
go: cannot find main module Выполнить go mod init в корне проекта
no Go files Путь ./cmd/server должен содержать package main

Distroless-вариант — энциклопедия, Go.


6. React / Vue SPA + nginx

Задача: собрать фронт (npm run build), раздавать статику через nginx; маршруты React Router работают при обновлении страницы.

Смысл простыми словами: браузеру нужны только файлы из dist/ (HTML, JS, CSS). Node.js в production не обязателен — он нужен лишь на этапе сборки. Поэтому два этапа: builder (Node) и runner (nginx).

Структура:

frontend/
├── package.json
├── package-lock.json
├── vite.config.js
├── index.html
├── src/
├── nginx.conf
└── Dockerfile

nginx.conf:

server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

Разбор nginx (зачем в лабораторной):

Строка Смысл
root /usr/share/nginx/html Сюда Dockerfile копирует dist/
try_files $uri $uri/ /index.html Если файла нет (маршрут /about) — отдать index.html, React дорисует страницу

Dockerfile:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html

EXPOSE 80
HEALTHCHECK CMD wget -qO- http://127.0.0.1/ || exit 1

Разбор по строкам:

Строка Смысл
npm ci + npm run build Сборка фронта в стадии builder
COPY --from=builder /app/dist В nginx попадает только результат сборки
nginx.confconf.d/default.conf Подмена дефолтного виртуального хоста
Нет CMD В образе nginx уже задана команда запуска

Сборка и проверка:

docker build -t spa:1 .
docker run --rm -p 8080:80 spa:1

Откройте http://localhost:8080. Для React Router зайдите на вложенный путь (если есть роуты) — без try_files будет 404 от nginx.

Если не работает:

Симптом Причина
Пустая страница Неверная папка — у Vite это dist, у Create React App тоже часто dist
404 на /about Нет try_files в nginx.conf
npm run build падает в Docker Не хватает памяти — закройте лишние программы

Подробнее proxy и TLS — Nginx — конфиги. Компоненты React — галерея React.


7. Java Spring Boot (JAR)

Задача: собрать fat JAR в образе с JDK, запускать на JRE.

Смысл простыми словами: Spring Boot упаковывает приложение в один .jar со встроенным Tomcat. В Dockerfile сначала Maven собирает JAR, потом в лёгкий образ копируется только app.jar и команда java -jar.

Структура:

spring-app/
├── pom.xml
├── mvnw
├── .mvn/
├── src/
└── Dockerfile

Dockerfile:

Разбор по строкам:

Строка Смысл
eclipse-temurin:21-jdk-alpine JDK для компиляции
./mvnw … package Сборка JAR без установленного Maven на ПК
-DskipTests Быстрее для учебной сборки
jre-alpine в финале Только среда выполнения, без компилятора
COPY … *.jar app.jar Один понятный файл в runtime
/actuator/health Эндпоинт Spring Actuator (нужна зависимость в pom.xml)
ENTRYPOINT ["java", "-jar", "app.jar"] Команда запуска; аргументы docker run дописываются в конец

Сборка и проверка:

docker build -t spring:1 .
docker run --rm -p 8080:8080 spring:1

Первая сборка может занять 5–15 минут — Maven скачивает зависимости.

Если не работает:

Ошибка Решение
no main manifest В pom.xml должен быть spring-boot-maven-plugin
Health 404 Добавить spring-boot-starter-actuator или убрать HEALTHCHECK
Несколько JAR в target/ Уточнить имя: COPY …/myapp-0.0.1-SNAPSHOT.jar app.jar

8. PHP с Apache

Задача: быстрый учебный сайт на PHP без настройки php-fpm и второго контейнера.

Смысл простыми словами: образ php:8.3-apache уже содержит веб-сервер Apache и модуль PHP. Вы копируете .php файлы в /var/www/html/ — Apache выполняет PHP и отдаёт результат браузеру.

Структура:

php-site/
├── public/
│   └── index.php
└── Dockerfile

public/index.php:

<?php
header('Content-Type: text/plain; charset=utf-8');
echo "PHP " . PHP_VERSION . " в Docker\n";
echo "Время: " . date('c') . "\n";

Dockerfile:

FROM php:8.3-apache

RUN docker-php-ext-install pdo pdo_mysql

COPY public/ /var/www/html/

EXPOSE 80

Разбор по строкам:

Строка Смысл
php:8.3-apache PHP 8.3 + Apache в одном образе
docker-php-ext-install pdo pdo_mysql Расширения для лабораторных с MySQL
COPY public/ /var/www/html/ index.php доступен как http://…/
EXPOSE 80 Apache слушает 80 внутри контейнера

Сборка и проверка:

docker build -t php-site:1 .
docker run --rm -p 8080:80 php-site:1

В браузере — версия PHP и время.

Если не работает:

Симптом Причина
Скачивается файл вместо выполнения Файл не .php или не в public/
403 Нет index.php в /var/www/html

Production-вариант — nginx + php-fpm в двух сервисах: Nginx PHP-FPM, WordPress Compose.


9. ASP.NET Core

Задача: собрать и опубликовать .NET 8 Web API в компактном runtime-образе.

Смысл простыми словами: стадия sdk компилирует проект (dotnet publish), стадия aspnet только запускает готовые DLL. Kestrel слушает порт из ASPNETCORE_URLS.

Структура:

dotnet-api/
├── DotnetApi.csproj
├── Program.cs
└── Dockerfile

Dockerfile:

Разбор по строкам:

Строка Смысл
COPY DotnetApi.csproj + dotnet restore Кэш NuGet до копирования всего кода
dotnet publish … -o /app/publish Готовые файлы для запуска
UseAppHost=false Запуск через dotnet MyApp.dll (проще в Linux-контейнере)
aspnet:8.0-alpine Runtime без SDK
ASPNETCORE_URLS=http://+:8080 Kestrel на всех интерфейсах, порт 8080
DotnetApi.dll Имя = имя проекта в .csproj

Сборка и проверка:

docker build -t dotnet-api:1 .
docker run --rm -p 8080:8080 dotnet-api:1

В Program.cs для учебы:

app.MapGet("/", () => "ASP.NET в Docker");
app.MapGet("/health", () => "ok");

Если не работает:

Ошибка Решение
could not find DotnetApi.dll Имя DLL = имя .csproj
Connection refused с хоста Проверить ASPNETCORE_URLS и -p 8080:8080

10. Job-контейнер (миграции или seed)

Задача: контейнер запускает скрипт и завершается — миграции БД, загрузка тестовых данных, ночной batch.

Смысл простыми словами: это не веб-сервер. Вы ждёте код выхода 0 (успех) и статус Exited (0) в docker ps -a. В Kubernetes тот же паттерн — ресурс Job.

Структура:

db-job/
├── migrate.sh
└── Dockerfile

migrate.sh:

#!/bin/sh
set -e
echo "Подключение: $DATABASE_URL"
# Раскомментируйте для реальной БД:
# psql "$DATABASE_URL" -f ./migrations/001_init.sql
echo "Миграции применены"

Dockerfile:

FROM alpine:3.19
RUN apk add --no-cache postgresql-client bash
WORKDIR /job
COPY migrate.sh .
RUN chmod +x migrate.sh
USER nobody
ENTRYPOINT ["./migrate.sh"]

Разбор по строкам:

Строка Смысл
apk add postgresql-client Утилита psql для SQL
chmod +x migrate.sh Скрипт исполняемый
USER nobody Даже одноразовая задача не от root
ENTRYPOINT ["./migrate.sh"] При docker run всегда стартует скрипт
Нет EXPOSE Сетевой сервер не поднимается

Запуск рядом с Postgres из Compose:

docker build -t db-job:1 .
docker run --rm \
  -e DATABASE_URL=postgres://app:secret@db:5432/appdb \
  --network myproject_default \
  db-job:1
Параметр Смысл
-e DATABASE_URL=… Пароль не в образе, только при запуске
--network … Имя db резолвится в IP контейнера PostgreSQL
--rm Удалить контейнер после успеха

Имя сети смотрите: docker network ls после docker compose up из галереи Compose.

Проверка: в выводе Миграции применены; docker ps -a — контейнер Exited (0).


Слои и кэш — одна таблица на все примеры

Правило Пример
Редко меняющееся — выше COPY package.json перед COPY src
Тяжёлое — отдельный слой RUN npm ci, RUN pip install
Секреты — не в образ .env в .dockerignore, -e при run
Фиксируйте версии node:20-alpine, не node:latest
Меньше финальный образ multi-stage для Go, Node, Java, .NET, SPA

Проверить слои:

docker history my-api:1 --no-trunc

Частые ошибки (все примеры)

Ошибка Что значит Что проверить
COPY failed: file not found Нет файла в контексте Путь, .dockerignore, вы в нужной папке
port is already allocated Порт занят на хосте Другой -p 8888:3000
Сайт не открывается Нет проброса порта -p хост:контейнер
API не отвечает с ПК Слушает только localhost 0.0.0.0 в коде
permission denied USER без прав на файлы chown перед USER
Огромный образ Всё в одной стадии Multi-stage, .dockerignore

Чек-лист перед сдачей лабораторной

  • В Dockerfile зафиксированы теги (node:20-alpine), не latest.
  • Есть .dockerignore, нет .env в образе.
  • Зависимости копируются до исходников.
  • В README — docker build, docker run, URL или curl для проверки.
  • Для HTTP указаны EXPOSE и -p.
  • Скриншот docker ps или ответа в браузере приложен к отчёту.

Связанные материалы

Материал Зачем
Dockerfile (теория) все инструкции, анти-паттерны
Docker build, run, push
Docker Compose — готовые стеки build: . + БД + nginx
Nginx — конфиги SPA, proxy, PHP-FPM
Шаблоны минимальный Dockerfile на одной странице
GitHub Actions — CI/CD docker build в pipeline