Docker starts today. After fourteen days of Git, the track moves to containers, and it opens with the least glamorous task available: install the packages and start the service.
Both halves of today turned out to be about time. When something starts, how long it takes, and what still holds after a reboot.
One Docker task, one AWS task. Install docker-ce and Compose on App Server 2, then build an EC2 and private RDS application stack that actually serves a page. The tasks come from the KodeKloud Engineer platform.
Install Docker, and the two verbs people conflate
sudo yum install -y yum-utils && \
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo && \
sudo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
docker-ce is not in the base RHEL or CentOS repositories, so the repo has to be added first, which is why those three commands are chained rather than pasted separately. Run the install first, and you get "No match for argument: docker-ce", which sends you hunting for a typo instead of a missing repository.
Four packages, four jobs. docker-ce is the daemon, docker-ce-cli is the client you type at, containerd.io is the runtime the daemon delegates to, and docker-compose-plugin is what makes docker compose a subcommand. That last one matters: docker compose with a space is Compose v2 shipped as a plugin, and docker-compose with a hyphen is the old standalone binary. Different programs. Any guide using the hyphenated form is telling you about v1.
Then:
sudo systemctl start docker && sudo systemctl enable docker
These are two different promises. start says the daemon is running now. enable says it comes back after a reboot. A task that says "start the service" almost always means both, and systemctl enable --now docker does the pair in one call.
Worth verifying each claim with the command that actually tests it:
docker -v # the client is installed
docker compose version # the plugin is installed
systemctl status docker # the daemon is running
docker -v answering does not mean the daemon is up. The client is a separate binary and never talks to it. Two different failures get conflated here: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? means the service is not started, while permission denied while trying to connect to the Docker daemon socket means it is running and your user is not in the docker group. Same symptom, opposite fixes.
An hour and eight minutes of it provisioning
The AWS half was four things that only count if all four work together: a private MySQL RDS instance, security groups joining it to an EC2, password-less root SSH, and an index.php that renders Connected successfully in a browser.
The lab window was an hour, and RDS takes five to ten minutes to provision. So the first command was the RDS create, and everything else happened while it built. Roughly eight minutes of provisioning cost about thirty seconds of actual waiting, because the SSH and Apache work filled the gap.
That generalises further than it looks: in any time-boxed environment, find the slowest asynchronous resource and start it before you start thinking about anything else.
One flag saved a whole step. The task lists "create a database named devops_db" separately, which reads like connecting a MySQL client to an endpoint you cannot yet reach:
aws rds create-db-instance ... --db-name devops_db
RDS creates the schema during provisioning. Note the naming trap while you are there: --db-instance-identifier is the RDS instance (devops-rds, hyphens) and --db-name is the MySQL schema (devops_db, underscore). Both read as "the database name" in English.
For the security group, reference the group rather than an address:
aws ec2 authorize-security-group-ingress \
--group-id $RDS_SG --protocol tcp --port 3306 --source-group $EC2_SG
--source-group means anything wearing that security group can connect. It survives the instance being replaced or its private IP changing, and grants nothing to anything else in the VPC. A hardcoded /32 works today and breaks the first time the instance is recycled.
Sixty seconds
The task says to connect from the console, which means the EC2 Instance Connect button. Its CLI equivalent has a property the console hides:
aws ec2-instance-connect send-ssh-public-key --region $REGION \
--instance-id $EC2_ID --instance-os-user ubuntu \
--ssh-public-key file:///root/.ssh/id_rsa.pub
That injects your public key into the user's authorized_keys for sixty seconds. It is a temporary grant, not an install, so the SSH has to be chained onto the same command with &&. Paste them as two commands, and you will usually miss the window. The pattern is to spend that one login installing the key permanently.
Also, do not guess the username:
aws ec2 describe-images --image-ids $AMI --query 'Images[0].Name' --output text
# ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20240701
Ubuntu, so ubuntu. My instinct was ec2-user, which is Amazon Linux, and it would have failed with Permission denied (publickey), the identical error you get from a genuinely broken key. Two unrelated problems, one message, one describe-images call to tell them apart.
And when installing the root key, overwrite rather than append. Cloud Ubuntu images ship /root/.ssh/authorized_keys pre-populated with a forced-command entry that prints "Please login as the user ubuntu" and hangs up. Appending leaves it in place and it fires first.
The one that gets everyone
index.php was in /var/www/html/, correct, and the browser kept showing the old default page.
DirectoryIndex index.html index.cgi index.pl index.php ...
index.html is listed first, and it already existed. Apache served it and never looked at the PHP file.
mv /var/www/html/index.html /var/www/html/index.html.bak
No reload needed. DirectoryIndex is evaluated per request against what is on disk, so the next request picks up index.php immediately.
The symptom to file away: a PHP file that is definitely present, definitely correct, and definitely not being served is a DirectoryIndex problem, not a PHP problem.
Finish from the shell rather than the browser:
aws rds wait db-instance-available --db-instance-identifier devops-rds && curl -s http://$EC2_IP/
Connected successfully<br />
That one line proves Apache is serving, PHP is executing, php-mysql is loaded, the security group is open, RDS is reachable, the credentials are valid and devops_db exists. A browser proves the same thing while adding caching and your own network as extra variables.
Everything here was a clock
start versus enable is a question about time. Firing the RDS create first is a question about time. The sixty-second key grant is a question about time. Even DirectoryIndex is ordering, just in a config file rather than a schedule.
So here is the Day 35 question. In the thing you deployed most recently, which parts survive a reboot, and do you know that because you checked or because it has not been rebooted yet?
Day 35 down. Sixty-five to go.
Top comments (2)
Docker is really fun. I got quite obsessed with it for a while and wanted to deploy every kind of middleware using Docker. The most recent project was building a platform that connects multimodal large models to run automated tests from natural‑language descriptions. Unfortunately the company would not cover the costs and it never got project approval. Honestly I have never dealt with full‑on server‑level restarts, I am just a regular QA engineer haha. That said, I just thought of that classic tech meme: restart your computer and it fixes 99 percent of your problems.🤣
😎 A great number of infrastructure mistakes come from accepting a promise without asking how long it lasts. "start" promises now. "enable" promises after reboot. A temporary SSH key promises sixty seconds. An IP rule promises connectivity while that IP remains meaningful.
Infrastructure becomes easier to reason about when every successful command is followed by one question: what does this guarantee, and for how long?