Inception Project — Comprehensive Deep-Dive
Repository: Seraph919/inception · Author: Ayoub Soudani (asoudani) · Context: 42 / 1337 curriculum project
Inception Project — Comprehensive Deep-Dive
Repository:
[Seraph919/inception](https://github.com/Seraph919/inception) · Author: Ayoub Soudani (asoudani) · Context: 42 / 1337 curriculum project
1. Repository Overview
Project Goal
Inception is a **42-school system-administration project whose objective is to deploy a fully functional web infrastructure inside a Linux virtual machine, using Docker Compose** exclusively. Every service must be isolated in its own container, built from a custom Dockerfile (no pulling pre-built application images), wired together over a private Docker bridge network, and persisted via Docker volumes stored under /home/<login>/data.
Expected Output / Evaluation Criteria
Requirement Detail Single HTTPS entry-point NGINX on port 443 (TLSv1.2/1.3 only) PHP CMS WordPress served via PHP-FPM Database MariaDB Persistence Named volumes backed by bind-mounts under $DATA_PATH Network isolation Single custom bridge network; no network_mode: host No pre-built app images All images are FROM debian:12 + apt packages No foreground-workaround processes Each container runs one real foreground daemon (no tail -f, sleep infinity) Bonus services Redis, FTP, Adminer, Static Site, Portainer
How It Is Run / Evaluated
make # builds images + starts all containers
make clean # docker compose down
make re # fclean then make
The evaluator checks: all containers are running, https://<login>.42.fr returns the WordPress site, volumes persist after a make clean && make, and bonus services are reachable on their respective ports.
Services / Containers at a Glance
Container Role Port(s) exposed to host nginx Reverse proxy + TLS termination 443 wordpress PHP-FPM application server none (internal 9000) mariadb MySQL-compatible database none (internal 3306) redis In-memory object cache for WP none (internal 6379) ftp vsftpd FTP server 21, 21000-21010 adminer DB admin web UI (PHP built-in server) 8080 static_site Static NGINX portfolio CV site 8081 portainer Docker management web UI 9000
2. Docker Architecture Deep Dive
2.1 The Stack: CLI → dockerd → containerd → runc
When you type docker compose up, the following chain fires:
┌─────────────────────────────────────────────────────────────────────────┐
│ USER SPACE (your shell / Makefile) │
│ │
│ docker compose up --build │
│ │ │
│ ▼ (Unix socket /var/run/docker.sock OR TCP :2376) │
│ ┌────────────────────┐ │
│ │ Docker CLI │ (client binary: /usr/bin/docker) │
│ │ docker-compose │ (compose plugin: builds OCI images first) │
│ └────────┬───────────┘ │
│ │ REST/gRPC API │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ dockerd (Docker daemon) │ │
│ │ • parses Compose YAML │ │
│ │ • manages image pulls / builds (calls BuildKit) │ │
│ │ • manages networks, volumes, secrets │ │
│ │ • owns /var/lib/docker (overlay2 image cache) │ │
│ │ │ gRPC (containerd.sock) │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ containerd │ │ │
│ │ │ • manages container lifecycle (create/start/stop) │ │ │
│ │ │ • snapshotter (overlayfs layers) │ │ │
│ │ │ • content store (OCI image blobs) │ │ │
│ │ │ │ spawns per-container │ │ │
│ │ │ ▼ │ │ │
│ │ │ ┌───────────────────────────────────────────────────┐ │ │ │
│ │ │ │ containerd-shim-runc-v2 (shim process) │ │ │ │
│ │ │ │ • stays alive even if containerd restarts │ │ │ │
│ │ │ │ • holds stdio/exit-code on behalf of container │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ ▼ (one-shot then exits) │ │ │ │
│ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ runc (OCI runtime) │ │ │ │ │
│ │ │ │ │ • creates namespaces & cgroups │ │ │ │ │
│ │ │ │ │ • sets up rootfs (overlayfs mount) │ │ │ │ │
│ │ │ │ │ • drops capabilities, applies seccomp │ │ │ │ │
│ │ │ │ │ • exec()s PID 1 (your CMD / ENTRYPOINT) │ │ │ │ │
│ │ │ │ └──────────────────────────────────────────────┘ │ │ │ │
│ │ │ └───────────────────────────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Key points:
dockerdnever touches the container process itself — it delegates tocontainerd.containerddelegates the actualclone()syscalls torunc.- The shim (
containerd-shim-runc-v2) is a thin process that persists as the parent of PID 1 inside the container. Ifcontainerdordockerdrestarts, the container keeps running because the shim holds the file descriptors. runcis an OCI runtime — it reads theconfig.jsongenerated by containerd, callsclone()with namespace flags, sets up the root filesystem, thenexec()s into PID 1 and exits.
2.2 OCI Images and Layers (OverlayFS)
A Docker image is a stack of read-only layer tarballs. When a container starts, containerd’s snapshotter creates:
Upper (writable) ← container's writes land here
───────────────
Layer N (read-only) ← last RUN/COPY in Dockerfile
Layer N-1
...
Layer 1 (base) ← FROM debian:12
These are merged by the kernel’s OverlayFS driver into a single unified filesystem the container sees at /. The upper layer is ephemeral unless a volume is mounted.
In /var/lib/docker/overlay2/ you can inspect each layer by its hash. The merged/ directory is the actual container rootfs.
2.3 Conceptual Process Tree
After make completes on the host VM:
systemd (PID 1, host)
└── dockerd
└── containerd
├── containerd-shim-runc-v2 (for nginx)
│ └── nginx: master process [PID 1 in nginx container]
│ └── nginx: worker process
├── containerd-shim-runc-v2 (for wordpress)
│ └── /entrypoint.sh → php-fpm8.2 -F [PID 1 in wp container]
│ └── php-fpm: pool workers (www-data)
├── containerd-shim-runc-v2 (for mariadb)
│ └── ./script.sh → mysqld [PID 1 in mariadb container]
├── containerd-shim-runc-v2 (for redis)
│ └── redis-server [PID 1 in redis container]
├── containerd-shim-runc-v2 (for ftp)
│ └── /entrypoint.sh → vsftpd [PID 1 in ftp container]
├── containerd-shim-runc-v2 (for adminer)
│ └── php -S 0.0.0.0:8080 [PID 1 in adminer container]
├── containerd-shim-runc-v2 (for static_site)
│ └── nginx -g 'daemon off;' [PID 1 in static_site container]
└── containerd-shim-runc-v2 (for portainer)
└── /opt/portainer/portainer [PID 1 in portainer container]
Each PID 1 listed above is in its own PID namespace — it sees itself as PID 1. From the host, these appear as regular processes with non-1 PIDs.
3. Linux Primitives Used by Docker
3.1 Namespaces
Docker/runc creates a set of namespaces for each container at clone() time:
Namespace Flag What it isolates pid CLONE_NEWPID Process IDs; container's init is PID 1 to itself net CLONE_NEWNET Network stack (interfaces, iptables, ports) mnt CLONE_NEWNS Filesystem mount table (overlayfs rootfs is the container's /) user CLONE_NEWUSER UID/GID mapping; allows rootless containers ipc CLONE_NEWIPC SysV IPC (shared memory, semaphores, message queues) uts CLONE_NEWUTS Hostname and domain name (container gets its own)
In this project every container gets its hostname set to its container_name (e.g. nginx, wordpress, mariadb). Internal DNS resolution on the inception bridge network resolves these names to container IPs, which is how wordpress can connect to mariadb:3306 and to redis:6379.
3.2 cgroups (v1/v2)
Control Groups limit and account resource usage. runc writes the container's PID into the appropriate cgroup subsystem directories:
- cpu — limits CPU shares / quota
- memory — limits RAM + swap; triggers OOM killer
- blkio / io — throttles block device I/O
- pids — caps the number of spawnable processes
This project does not declare explicit mem_limit or cpus in docker-compose.yml, so containers inherit the host default (unrestricted), but they are still placed in cgroups so Docker can account for them and enforce the PID namespace.
3.3 Capabilities
Linux capabilities split root privileges into fine-grained tokens. By default Docker grants a restricted set (e.g. CAP_CHOWN, CAP_NET_BIND_SERVICE, CAP_SETUID, CAP_SETGID) and drops dangerous ones (CAP_SYS_ADMIN, CAP_NET_ADMIN, etc.). No container in this project requests --cap-add, so the default restricted set applies.
The MariaDB script (srcs/requirements/mariadb/tools/script.sh, line 27) calls chown -R mysql:mysql /var/lib/mysql which requires CAP_CHOWN — present in the default set.
3.4 seccomp / AppArmor
Docker applies a default seccomp profile that blocks ~44 syscalls (e.g. ptrace, kexec_load, reboot). The vsftpd container explicitly sets seccomp_sandbox=NO in vsftpd.conf.template (line 16) because vsftpd's own internal seccomp sandbox conflicts with Docker's.
AppArmor profiles (if the host has AppArmor enabled) add a MAC layer; Docker uses its docker-default AppArmor profile unless overridden.
3.5 OverlayFS
Described above in §2.2. Each container’s writable layer lives at /var/lib/docker/overlay2/<id>/diff/. Volumes (like /var/www/html and /var/lib/mysql) bypass OverlayFS entirely — they are bind-mounted directly from the host path into the container's mount namespace, providing persistence and performance.
4. Project Implementation Details
Directory Layout
inception/
├── Makefile
├── README.md
├── DEV_DOC.md
├── USER_DOC.md
├── srcs/
│ ├── .env.example ← template for srcs/.env (actual .env is gitignored)
│ ├── docker-compose.yml
│ └── requirements/
│ ├── nginx/
│ │ ├── Dockerfile
│ │ ├── conf/default ← nginx vhost template
│ │ └── tools/entrypoint.sh
│ ├── wordpress/
│ │ ├── Dockerfile
│ │ ├── conf/www.conf ← PHP-FPM pool config
│ │ └── tools/script.sh
│ ├── mariadb/
│ │ ├── Dockerfile
│ │ ├── conf/50-server.cnf ← MariaDB server config
│ │ └── tools/script.sh
│ └── bonus/
│ ├── redis/
│ │ ├── Dockerfile
│ │ └── conf/redis.conf
│ ├── ftp/
│ │ ├── Dockerfile
│ │ ├── conf/vsftpd.conf.template
│ │ └── tools/entrypoint.sh
│ ├── adminer/
│ │ ├── Dockerfile
│ │ └── index.php ← thin wrapper that pre-selects mariadb host
│ ├── static-site/
│ │ ├── Dockerfile
│ │ ├── conf/default.conf
│ │ └── site/
│ │ ├── index.html ← Ayoub Soudani's CV portfolio
│ │ └── profile.jpg
│ └── portainer/
│ └── Dockerfile
└── secrets/ ← *.txt password files (gitignored)
4.1 NGINX
File: srcs/requirements/nginx/Dockerfile
FROM debian:12
RUN apt-get update \
&& apt-get install -y --no-install-recommends nginx ssl-cert gettext-base \
&& rm -rf /var/lib/apt/lists/*
RUN make-ssl-cert generate-default-snakeoil --force-overwrite
COPY conf/default /etc/nginx/templates/default.template
COPY tools/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
CMD ["/entrypoint.sh"]
Packages installed:
nginx— the web serverssl-cert— providesmake-ssl-certand thesnakeoilself-signed certificate toolgettext-base— providesenvsubstfor template rendering
TLS: make-ssl-cert generate-default-snakeoil generates /etc/ssl/certs/ssl-cert-snakeoil.pem and /etc/ssl/private/ssl-cert-snakeoil.key at build time. The nginx config includes them via include snippets/snakeoil.conf; (Debian standard snippet).
Entrypoint (srcs/requirements/nginx/tools/entrypoint.sh):
envsubst '${DOMAIN_NAME}' < /etc/nginx/templates/default.template \
> /etc/nginx/sites-enabled/default
exec nginx -g 'daemon off;'
- Renders the vhost template, substituting
$DOMAIN_NAMEat runtime. execreplaces the shell with nginx so nginx is PID 1 (correct signal handling).
VHost config (srcs/requirements/nginx/conf/default):
listen 443 ssl;
include snippets/snakeoil.conf;
ssl_protocols TLSv1.2 TLSv1.3;
server_name ${DOMAIN_NAME}; # rendered by envsubst
root /var/www/html;
index index.php index.html ...;
location / {
try_files $uri $uri/ /index.php?$args; # WordPress permalink support
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass wordpress:9000; # PHP-FPM upstream
}
Volumes / Networks / Ports:
Value Port (host→container) 443:443 Volume wordpress_files:/var/www/html:ro (read-only; WP writes via the wordpress container) Network inception (bridge) Secrets wp_admin_password, wp_user_password depends_on wordpress
Data persistence: None. NGINX is stateless; WordPress files are served from the shared wordpress_files volume.
4.2 WordPress (PHP-FPM)
File: srcs/requirements/wordpress/Dockerfile
FROM debian:12
RUN apt-get install -y --no-install-recommends \
php-fpm php-mysql php-redis curl ca-certificates mariadb-client
RUN curl -fsSL -o /usr/local/bin/wp \
https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
&& chmod +x /usr/local/bin/wp
COPY conf/www.conf /etc/php/8.2/fpm/pool.d/.
COPY tools/script.sh ./script.sh
CMD ["./script.sh"]
Packages: php8.2-fpm, php-mysql (PDO + mysqli), php-redis (phpredis extension), mariadb-client (for mysqladmin ping healthcheck in script), wp-cli (WP management CLI).
PHP-FPM pool (srcs/requirements/wordpress/conf/www.conf):
listen = 9000— TCP socket (not unix socket), so NGINX can connect viawordpress:9000listen.owner = www-data,listen.group = www-datauser = www-datapm = dynamicwith defaultmax_children = 5
Initialization script (srcs/requirements/wordpress/tools/script.sh):
1. Reads passwords from /run/secrets/{mysql_password,wp_admin_password,wp_user_password}
2. Validates WP_ADMIN_USER does not contain "admin" (42 rule)
3. Waits for MariaDB (mysqladmin ping -hmariadb loop)
4. Downloads WordPress core if not already present (wp core download --allow-root)
5. Creates wp-config.php if missing (wp config create ... --dbhost=mariadb)
6. Installs WP if not installed (wp core install ... title, admin, email)
7. Creates a second "author" user ($WP_USER)
8. Sets siteurl and home to $WP_URL
9. Configures FS_METHOD=direct and WP_REDIS_HOST=redis in wp-config.php
10. Installs + activates redis-cache plugin; enables Redis
11. Sets ownership to www-data:www-data
12. exec php-fpm8.2 -F ← foreground; becomes PID 1
Environment variables used:
Variable Source Purpose MYSQL_DATABASE .env WordPress database name MYSQL_USER .env DB username WP_TITLE .env Site title WP_ADMIN_USER .env WP admin username WP_ADMIN_EMAIL .env WP admin email WP_USER .env Second WP user (author) WP_USER_EMAIL .env Second user email DOMAIN_NAME .env Used to build WP_URL mysql_password secret DB password read from /run/secrets/mysql_password wp_admin_password secret Admin password wp_user_password secret Author password
Volumes / Networks / Ports:
Value Port none exposed to host Volume wordpress_files:/var/www/html (read-write) Network inception depends_on mariadb, redis
Data persistence: wordpress_files volume → host path $DATA_PATH/wordpress. WordPress core, plugins, uploads, and wp-config.php all live here. On restart, the script detects wp-settings.php exists and skips re-download; it also skips re-install if wp core is-installed returns true.
4.3 MariaDB
File: srcs/requirements/mariadb/Dockerfile
FROM debian:12
RUN apt-get install -y --no-install-recommends mariadb-server
COPY conf/50-server.cnf /etc/mysql/mariadb.conf.d/.
COPY tools/script.sh ./script.sh
RUN mkdir /run/mysqld && chmod +x script.sh
CMD ["./script.sh"]
Server config (srcs/requirements/mariadb/conf/50-server.cnf):
bind-address = 0.0.0.0— listens on all interfaces inside the containercharacter-set-server = utf8mb4,collation-server = utf8mb4_general_cidatadir = /var/lib/mysql,pid-file = /run/mysqld/mysqld.pid- Bin logging:
expire_logs_days = 10
Initialization script (srcs/requirements/mariadb/tools/script.sh):
1. Reads passwords from /run/secrets/{mysql_root_password,mysql_password}
2. Validates MYSQL_DATABASE and MYSQL_USER are set
3. First-run detection: checks whether /var/lib/mysql/mysql/ directory exists
- If not (FIRST_INIT=true): runs mysql_install_db to initialize data directory
4. Starts mysqld in background (&) and waits up to 30s for ping
5. On first init: runs SQL without password to:
CREATE DATABASE, CREATE USER, GRANT, set root password
6. On subsequent starts: runs SQL with root password (already set)
7. Kills the background mysqld gracefully
8. exec mysqld --user=mysql --bind-address=0.0.0.0 ← becomes PID 1
This “start background, configure, stop, re-exec in foreground” pattern is the standard way to run one-time DB initialization without needing an entrypoint that stays as the parent.
Volumes / Networks / Ports:
Value Port none exposed to host Volume mariadb_data:/var/lib/mysql Network inception Secrets mysql_root_password, mysql_password
Data persistence: mariadb_data → $DATA_PATH/mariadb. Actual InnoDB data files, ibdata1, ib_logfile*, and the wordpress schema directory all live here. On restart, FIRST_INIT=false and the script skips mysql_install_db, going straight to verifying/updating the user.
4.4 Redis (Bonus)
File: srcs/requirements/bonus/redis/Dockerfile
FROM debian:12
RUN apt-get install -y --no-install-recommends redis-server ca-certificates
COPY conf/redis.conf /etc/redis/redis.conf
CMD ["redis-server", "/etc/redis/redis.conf"]
Config (srcs/requirements/bonus/redis/conf/redis.conf):
bind 0.0.0.0 # listen on all interfaces
port 6379
protected-mode no # no AUTH required (only accessible within inception network)
save "" # disable RDB persistence (volatile cache)
appendonly no # disable AOF
daemonize no # stay in foreground (PID 1)
Redis acts as a WordPress object cache via the redis-cache plugin installed by the WordPress entrypoint. WordPress connects to redis:6379 (set via WP_REDIS_HOST=redis in wp-config.php).
Volumes / Networks / Ports:
Value Port none exposed to host Volume none (in-memory cache, no persistence) Network inception depends_on none
4.5 FTP / vsftpd (Bonus)
File: srcs/requirements/bonus/ftp/Dockerfile
FROM debian:12
RUN apt-get install -y --no-install-recommends vsftpd gettext-base
COPY conf/vsftpd.conf.template /etc/vsftpd.conf.template
COPY tools/entrypoint.sh /entrypoint.sh
CMD ["/entrypoint.sh"]
Config template (srcs/requirements/bonus/ftp/conf/vsftpd.conf.template):
Key settings:
local_enable=YES,write_enable=YES— allow authenticated local users to writechroot_local_user=YES,allow_writeable_chroot=YES— jail user to their home (/var/www/html)local_root=/var/www/html— user's root = WordPress filesseccomp_sandbox=NO— disables vsftpd's internal seccomp to avoid conflict with Docker's seccomppasv_enable=YES,pasv_min_port=21000,pasv_max_port=21010— passive mode for data channelpasv_address=${FTP_PASV_ADDRESS}— rendered byenvsubstat container start
Entrypoint (srcs/requirements/bonus/ftp/tools/entrypoint.sh):
envsubst '${FTP_PASV_ADDRESS}' < /etc/vsftpd.conf.template > /etc/vsftpd.conf
FTP_PASSWORD="$(cat /run/secrets/ftp_password)"
echo "/usr/sbin/nologin" >> /etc/shells
useradd -m -d /var/www/html -s /usr/sbin/nologin "${FTP_USER}"
echo "${FTP_USER}:${FTP_PASSWORD}" | chpasswd
chown -R "${FTP_USER}:${FTP_USER}" /var/www/html
exec /usr/sbin/vsftpd /etc/vsftpd.conf
The FTP user is created at runtime (not baked into the image) so the username and password come from env/secrets. The user’s shell is set to /usr/sbin/nologin — no SSH, FTP only.
Volumes / Networks / Ports:
Value Ports 21:21, 21000-21010:21000-21010 Volume wordpress_files:/var/www/html (shared with WordPress) Network inception Secrets ftp_password depends_on wordpress (ensures volume has content)
4.6 Adminer (Bonus)
File: srcs/requirements/bonus/adminer/Dockerfile
FROM debian:12
RUN apt-get install -y --no-install-recommends php-cli php-mysql curl ca-certificates
WORKDIR /var/www/html
RUN curl -fsSL -o adminer.php \
https://github.com/vrana/adminer/releases/download/v4.8.1/adminer-4.8.1.php
COPY index.php /var/www/html/index.php
CMD ["php", "-S", "0.0.0.0:8080", "-t", "/var/www/html"]
index.php (srcs/requirements/bonus/adminer/index.php):
<?php
if (!isset($_GET['server'])) {
$_GET['server'] = 'mariadb'; // pre-fills "Server" field with Docker DNS name
}
require __DIR__ . '/adminer.php';
This thin wrapper pre-selects mariadb as the server so the user doesn't have to type it. Adminer v4.8.1 is downloaded at image build time.
Volumes / Networks / Ports:
Value Port 8080:8080 Volume none Network inception depends_on mariadb
4.7 Static Site (Bonus)
File: srcs/requirements/bonus/static-site/Dockerfile
FROM debian:12
RUN apt-get install -y --no-install-recommends nginx
COPY site/ /var/www/html/
COPY conf/default.conf /etc/nginx/sites-enabled/default
CMD ["nginx", "-g", "daemon off;"]
Nginx config (srcs/requirements/bonus/static-site/conf/default.conf):
server {
listen 80;
server_name _;
root /var/www/html;
index index.html;
location / { try_files $uri $uri/ =404; }
}
Plain HTTP, no TLS. Content is Ayoub Soudani’s personal CV/portfolio (site/index.html — a single-page HTML with embedded CSS). Photo at site/profile.jpg.
Volumes / Networks / Ports:
Value Port 8081:80 Volume none Network inception
4.8 Portainer (Bonus)
File: srcs/requirements/bonus/portainer/Dockerfile
FROM debian:12
ARG PORTAINER_VERSION=2.33.6
RUN apt-get install -y --no-install-recommends ca-certificates curl tar
RUN arch="$(dpkg --print-architecture)"; \
url="https://github.com/portainer/portainer/releases/download/..."; \
curl -fsSL "${url}" -o /tmp/portainer.tgz; \
tar -xzf /tmp/portainer.tgz -C /opt/portainer --strip-components=1
CMD ["/opt/portainer/portainer"]
Portainer v2.33.6 binary is downloaded at build time; architecture is auto-detected (amd64/arm64/armhf). The binary is the only process, starting the UI on port 9000.
Volumes / Networks / Ports:
Value Port 9000:9000 Volumes /var/run/docker.sock:/var/run/docker.sock (Docker socket — allows Portainer to manage Docker), portainer_data:/data (Portainer's own state) Network inception
⚠️ Mounting
/var/run/docker.sockgives Portainer (and any compromised container) root-equivalent access to the host's Docker daemon. In production, restrict access to the Portainer port (9000) to trusted networks only.
5. Config & Secrets
5.1 Environment Variables — srcs/.env
The .env file is gitignored (srcs/.env is listed in .gitignore). An example is provided at srcs/.env.example:
LOGIN=login
DATA_PATH=/home/login/data # host path for volume data; must be /home/<login>/data
DOMAIN_NAME=login.42.fr # resolved to VM IP via /etc/hosts
MYSQL_DATABASE=wordpress
MYSQL_USER=wpuser
WP_TITLE=inception
WP_ADMIN_USER=xxx # must NOT contain "admin"
WP_ADMIN_EMAIL=xxx@gmail.com
WP_USER=rand
WP_USER_EMAIL=rand@gmail.com
FTP_USER=ftpuser
FTP_PASV_ADDRESS=127.0.0.1 # public IP/hostname for FTP passive mode
These variables are passed into containers via env_file: .env in Compose. The Makefile also reads DATA_PATH directly with a shell grep to create host directories before starting Compose.
5.2 Docker Secrets — secrets/*.txt
Source: Local plaintext files under secrets/ directory (gitignored by secrets/*.txt in .gitignore).
Declared in srcs/docker-compose.yml:
secrets:
mysql_root_password:
file: ../secrets/mysql_root_password.txt
mysql_password:
file: ../secrets/mysql_password.txt
ftp_password:
file: ../secrets/ftp_password.txt
wp_admin_password:
file: ../secrets/wp_admin_password.txt
wp_user_password:
file: ../secrets/wp_user_password.txt
Docker reads these files and mounts them as tmpfs files inside each container at /run/secrets/<name>. They are not environment variables (visible in docker inspect) — they are files, which is safer.
Secret Mounted in Used by mysql_root_password mariadb MariaDB root password setup mysql_password mariadb, wordpress WP ↔ DB auth wp_admin_password nginx, wordpress WordPress admin user creation wp_user_password nginx, wordpress WordPress author user creation ftp_password ftp vsftpd user password
5.3 TLS / Certificates
TLS is handled entirely inside the nginx container at image build time:
RUN make-ssl-cert generate-default-snakeoil --force-overwrite
This generates:
/etc/ssl/certs/ssl-cert-snakeoil.pem(self-signed cert, CN=localhost)/etc/ssl/private/ssl-cert-snakeoil.key
The nginx vhost includes them via include snippets/snakeoil.conf;. These are self-signed certificates — browsers will show a warning. For production, replace with Let's Encrypt certs.
No external CA or cert-generation script is involved. The cert is baked into the image layer and is not stored in a volume or mounted from host.
6. Developer Workflow
6.1 Initial Setup (From Scratch)
# 1. Clone the repo
git clone https://github.com/Seraph919/inception.git
cd inception
# 2. Copy and fill the .env file
cp srcs/.env.example srcs/.env
# Edit srcs/.env: set LOGIN, DATA_PATH=/home/<your_login>/data,
# DOMAIN_NAME=<your_login>.42.fr, WP_ADMIN_USER (no "admin" in name)
# 3. Create secret files
mkdir -p secrets
echo "SuperRootPass123!" > secrets/mysql_root_password.txt
echo "WpDbPass456!" > secrets/mysql_password.txt
echo "AdminPass789!" > secrets/wp_admin_password.txt
echo "UserPass000!" > secrets/wp_user_password.txt
echo "FtpPass321!" > secrets/ftp_password.txt
# 4. Add domain to /etc/hosts
echo "127.0.0.1 asoudani.42.fr" | sudo tee -a /etc/hosts
# (replace with VM's actual IP if evaluating from outside the VM)
6.2 Makefile Targets
Target Command Description make / make all docker compose ... up -d --build Build images + start containers (creates $DATA_PATH/mariadb etc. first) make clean docker compose ... down Stop + remove containers and networks (volumes kept) make restart clean + all Down then rebuild and start make remove docker compose ... down --volumes --remove-orphans Remove containers + named volumes make wipe docker system prune -a --volumes Dangerous — removes ALL Docker resources on the host make fclean clean + rm -rf $DATA_PATH Removes containers and host data dirs make re fclean + all Full clean rebuild
6.3 Per-Service Debugging Commands
NGINX:
docker logs nginx
docker exec -it nginx nginx -t # test config syntax
docker exec -it nginx cat /etc/nginx/sites-enabled/default
curl -kvI https://asoudani.42.fr # check TLS handshake
WordPress:
docker logs wordpress
docker exec -it wordpress bash
docker exec -it wordpress wp --info --allow-root
docker exec -it wordpress wp plugin status redis-cache --allow-root
MariaDB:
docker logs mariadb
docker exec -it mariadb mysqladmin ping -uroot -p$(cat secrets/mysql_root_password.txt)
docker exec -it mariadb mysql -uroot -p$(cat secrets/mysql_root_password.txt) \
-e "SHOW DATABASES;"
docker exec -it mariadb mysql -uroot -p$(cat secrets/mysql_root_password.txt) wordpress \
-e "SELECT user_login, user_email FROM wp_users;"
Redis:
docker logs redis
docker exec -it redis redis-cli ping # → PONG
docker exec -it redis redis-cli info server
docker exec -it redis redis-cli monitor # live command stream
FTP:
docker logs ftp
ftp -p 127.0.0.1 # passive mode from host
# then: user ftpuser <password>
Adminer: Open http://<vm-ip>:8080 in browser. Server auto-filled to mariadb.
Portainer: Open http://<vm-ip>:9000 — create admin password on first visit.
6.4 Inspect Volumes / Data
# See where Docker thinks volumes are
docker volume inspect mariadb_data
docker volume inspect wordpress_files
# Browse host data directly
ls -la /home/<login>/data/mariadb
ls -la /home/<login>/data/wordpress
# See all containers in project
docker compose -p inception --env-file srcs/.env -f srcs/docker-compose.yml ps
6.5 View Compose Config (Resolved Variables)
docker compose --env-file srcs/.env -f srcs/docker-compose.yml config
6.6 Network Inspection
docker network inspect inception
# Shows all containers and their IPs on the inception bridge
docker exec -it nginx getent hosts wordpress # DNS resolution check
docker exec -it wordpress getent hosts mariadb
7. Quick Start
# Prerequisites: Docker + Docker Compose + sudo
git clone https://github.com/Seraph919/inception.git && cd inception
cp srcs/.env.example srcs/.env
# --- Edit srcs/.env to match your login and VM ---
mkdir -p secrets
echo "MyR00tPwd!" > secrets/mysql_root_password.txt
echo "MyDbPwd!" > secrets/mysql_password.txt
echo "MyAdmPwd!" > secrets/wp_admin_password.txt
echo "MyUsrPwd!" > secrets/wp_user_password.txt
echo "MyFtpPwd!" > secrets/ftp_password.txt
echo "127.0.0.1 asoudani.42.fr" | sudo tee -a /etc/hosts
make
# Wait ~60s for WordPress to initialize
curl -k https://asoudani.42.fr # should return WordPress HTML
After make:
- 🌐 WordPress:
[https://asoudani.42.fr](https://asoudani.42.fr) - 🔧 Adminer:
[http://localhost:8080](http://localhost:8080) - 📁 FTP:
ftp://localhost:21 - 🖥️ Static site (CV):
[http://localhost:8081](http://localhost:8081) - 🐳 Portainer:
[http://localhost:9000](http://localhost:9000)
8. Common Pitfalls
Problem Cause Fix WP_ADMIN_USER must not contain 'admin' Admin username contains "admin" literally (case-insensitive) Change WP_ADMIN_USER in .env Missing DOMAIN_NAME env var (nginx crash) .env not loaded or DOMAIN_NAME empty Check srcs/.env exists and has the variable WordPress shows "Error establishing a database connection" MariaDB not ready / wrong credentials Check docker logs mariadb; verify .env matches secret files Redis not connecting Redis container not started, or redis-cache plugin not activated docker exec -it wordpress wp redis status --allow-root FTP passive mode doesn't work FTP_PASV_ADDRESS is 127.0.0.1 but client connects from outside VM Set FTP_PASV_ADDRESS to the VM's actual external IP make wipe deleted everything docker system prune -a --volumes is global Never use make wipe on a shared Docker host Portainer asks to create password again portainer_data volume was removed Use make restart without make remove; don't delete portainer volume make fails: DATA_PATH empty srcs/.env missing or DATA_PATH line absent Check .env file exists with correct DATA_PATH= line Self-signed cert browser warning make-ssl-cert generates a localhost cert Expected for dev/school env; accept the warning or add cert to browser trust store FTP upload permission denied www-data owns /var/www/html but FTP user is ftpuser The entrypoint runs chown -R ${FTP_USER}:${FTP_USER} /var/www/html — check FTP logs make builds from old cache after Dockerfile change Docker layer cache docker compose build --no-cache <service> or make wipe && make Container exits immediately Script hits set -e on error docker logs <container> to see the error; check secret files exist and are non-empty
Document is from source inspection of Seraph919/inception.
메타데이터
- post_id
- 2f7e9d3cdfee
- slug
- inception-project-comprehensive-deep-dive-2f7e9d3cdfee
- url
- https://medium.com/@Seraph919/inception-project-comprehensive-deep-dive-2f7e9d3cdfee
- canonical_url
- https://medium.com/@Seraph919/inception-project-comprehensive-deep-dive-2f7e9d3cdfee
- author_url
- https://medium.com/@Seraph919
- status
- ok
- fetched_at
- 2026-06-22 07:15:07