Skip to content

Commit 330d550

Browse files
committed
Added 02-adding-containerized-services.md
1 parent 8e22656 commit 330d550

File tree

3 files changed

+372
-2
lines changed

3 files changed

+372
-2
lines changed
Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
# Adding containerized services
2+
3+
Our application is fairly simple right now. It has no backing database or other services it depends on. Rarely are apps this independent.
4+
5+
Fortunately, a change request has just come in!
6+
7+
> **CHANGE REQUEST INCOMING!** We would like to be able to update the memes displayed on the website without having to update code and redeploy the app. Also, if we get massive traffic, we want to ensure every instance of the app is using the same collection of images.
8+
9+
10+
## πŸ™‹ Breaking down the request
11+
12+
With a request like this, there are then a lot of follow-up questions that will likely come up, such as:
13+
14+
- Do we need an admin interface to manage the memes?
15+
- How should the memes be defined? Do we use a database? If so, which technology? If not, how will apps get the updated config?
16+
- Who should be able to update the available memes?
17+
18+
Fortunately, our Product and Engineering Leads decided on the following:
19+
20+
1. No. We do not need an admin interface. Direct database updates are fine for now. Let's just get something out as quickly as possible.
21+
2. Let's go ahead and use a database, as that's easy to deploy. Since we use PostgreSQL in other apps, we'll go with that.
22+
23+
So, the big questions are now...
24+
25+
- How do we get everyone's development environment updated to have a database?
26+
- How do we ensure everyone is using the same version of the database?
27+
- How can we provide tooling to help folks interact with the database?
28+
29+
Short answer... enter Docker and containers! 🐳 πŸ“¦
30+
31+
## Starting PostgreSQL
32+
33+
Running a PostgreSQL database in a container isn't too difficult.
34+
35+
1. Use the `docker run` command in a terminal to start a PostgreSQL container:
36+
37+
```bash
38+
docker run -d --name=postgres postgres:17-alpine
39+
```
40+
41+
This command is using the following flags:
42+
43+
- `-d` - run the container in "detached" mode. This runs the container in the background.
44+
- `--name postgres` - give this container a specific name. Normally, this flag is skipped and an auto-generated name is used. But, it helps in workshops.
45+
- `postgres:17-alpine` - this is the name of the container image to run
46+
47+
The output that you see is the full container ID.
48+
49+
2. To view the running containers, you use the `docker ps` command:
50+
51+
```console
52+
docker ps
53+
```
54+
55+
After running the previous command, you should see output similar to the following:
56+
57+
```plaintext
58+
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
59+
```
60+
61+
Where is the container we just ran? Maybe it didn't start successfully?
62+
63+
3. View all containers, even those that are no longer running, by adding the `-a` flag to the command:
64+
65+
```console
66+
docker ps -a
67+
```
68+
69+
With that, you should now see output similar to the following:
70+
71+
```plaintext
72+
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
73+
f8c7b5660668 postgres:17-alpine "docker-entrypoint.s…" 2 seconds ago Exited (1) 2 seconds ago postgres
74+
```
75+
76+
There it is! As we thought, the container did fail to start (per the `STATUS` column). Let's see if we can figure out what's going on.
77+
78+
4. View the logs of the container by using the `docker logs` command:
79+
80+
```console
81+
docker logs postgres
82+
```
83+
84+
The `docker logs` command requires either the name of a container or the ID. Since we previously named the container `postgres`, we were able to reference it with that name here.
85+
86+
In the log output, you should see something similar to the following:
87+
88+
```plaintext
89+
Error: Database is uninitialized and superuser password is not specified.
90+
You must specify POSTGRES_PASSWORD to a non-empty value for the
91+
superuser. For example, "-e POSTGRES_PASSWORD=password" on "docker run".
92+
93+
You may also use "POSTGRES_HOST_AUTH_METHOD=trust" to allow all
94+
connections without a password. This is *not* recommended.
95+
96+
See PostgreSQL documentation about "trust":
97+
https://www.postgresql.org/docs/current/auth-trust.html
98+
```
99+
100+
This error message is telling us that the container requires the definition of an environment variable named `POSTGRES_PASSWORD`.
101+
102+
5. Since we can't modify the environment variables for an existing container, we will have to create a new one. Use the following command to create a new container, but this time with the required variable:
103+
104+
```console
105+
docker run -d --name=postgres -e POSTGRES_PASSWORD=secret postgres:17-alpine
106+
```
107+
108+
When you run this command, you will get an error that looks similar to the following:
109+
110+
```plaintext
111+
docker: Error response from daemon: Conflict. The container name "/postgres" is already in use by container "f8c7b5660668324140f773b0a54a723bfe069a4d71ba231ca2ec8c4f33ddd314". You have to remove (or rename) that container to be able to reuse that name.
112+
```
113+
114+
We got this because we tried to use the same name as the previous container and names must be unique. This is why we generally don't specify names for our containers.
115+
116+
6. Remove the previous container using the `docker rm` command:
117+
118+
```console
119+
docker rm postgres
120+
```
121+
122+
7. Run the previous command again to start our database container:
123+
124+
```console
125+
docker run -d --name=postgres -e POSTGRES_PASSWORD=secret postgres:17-alpine
126+
```
127+
128+
This time, it should stay up and running! Hooray!
129+
130+
131+
## Exposing PostgreSQL
132+
133+
Now that we have a database running, let's try to connect our application to it.
134+
135+
1. Before connecting our app, we can validate the connection by using the `psql` tool. Run the following command to connect to the database:
136+
137+
```console
138+
psql -h localhost -U postgres
139+
```
140+
141+
Unfortunately, we're not able to connect. That's because we didn't _publish_ the container's database port - it's running only in its isolated environment.
142+
143+
2. Stop the container by running the following command:
144+
145+
```bash
146+
docker rm -f postgres
147+
```
148+
149+
The `-f` flag will stop the container first and then remove it.
150+
151+
3. Let's start a new container, but this time adding the `-p` flag to "publish" the port. This basically punches a hole through the network isolation to allow us to connect to the database. Run this command to do so:
152+
153+
```bash
154+
docker run -d --name=postgres -p 5432:5432 -e POSTGRES_PASSWORD=secret postgres:17-alpine
155+
```
156+
157+
4. Now, try to connect to the database using `psql`:
158+
159+
```bash
160+
psql -h localhost -U postgres
161+
```
162+
163+
When you're prompted for the password, enter the password we defined in the command:
164+
165+
```bash
166+
secret
167+
```
168+
169+
You should now be connected! It worked! πŸŽ‰
170+
171+
5. Disconnect from the database by running the following command from inside the `psql` tool:
172+
173+
```bash
174+
\q
175+
```
176+
177+
178+
## βž• Populating the database
179+
180+
Having a database is great, but it needs tables and data to actually be useful. How do we create those tables and populate initial data?
181+
182+
With Docker's database images, we can load "seed" files into the container and have it automatically create tables and provide data.
183+
184+
Let's give it a try! We'll create the schema files and then update our database to have them.
185+
186+
1. In our project, create a folder named `db`. You can either do so using the IDE directly or by running the following command:
187+
188+
```bash
189+
mkdir db
190+
```
191+
192+
2. In the `db` folder, create a file named `01-create-schema.sql` with the following contents:
193+
194+
```sql
195+
CREATE TABLE memes (
196+
"id" SERIAL NOT NULL PRIMARY KEY,
197+
"url" varchar(255) NOT NULL,
198+
"creation_date" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP
199+
);
200+
```
201+
202+
This will create a simple table named `memes` that will have three columns - the ID of the meme, its URL, and a timestamp for when it was created.
203+
204+
3. In the `db` folder, create a file named `02-initial-data.sql` with the following contents:
205+
206+
```sql
207+
INSERT INTO memes (url) VALUES
208+
('https://media.giphy.com/media/kyLYXonQYYfwYDIeZl/giphy.gif'),
209+
('https://media.giphy.com/media/IwAZ6dvvvaTtdI8SD5/giphy.gif'),
210+
('https://media.giphy.com/media/14hs7g86sQqDF6/giphy.gif');
211+
```
212+
213+
This will insert three memes into the table, specifying only the URLs. The ID and creation timestamps are automatically generated for us by the database.
214+
215+
4. Let's update the database to use these files. First, remove the current database container:
216+
217+
```bash
218+
docker rm -f postgres
219+
```
220+
221+
5. Use the following command to share the schema files from our workspace into the container (this is called bind mounting):
222+
223+
```bash
224+
docker run -d --name=postgres \
225+
-p 5432:5432 \
226+
-v ./db:/docker-entrypoint-initdb.d \
227+
-e POSTGRES_PASSWORD=secret \
228+
postgres:17-alpine
229+
```
230+
231+
6. Use the following `psql` command to validate the table exists and the data is there now:
232+
233+
```bash
234+
psql -h localhost -U postgres -c "SELECT * FROM memes"
235+
```
236+
237+
After entering the password (`secret`), you should see output similar to the following:
238+
239+
```plaintext
240+
id | url | creation_date
241+
----+------------------------------------------------------------+---------------
242+
1 | https://media.giphy.com/media/kyLYXonQYYfwYDIeZl/giphy.gif | 2025-08-19
243+
2 | https://media.giphy.com/media/IwAZ6dvvvaTtdI8SD5/giphy.gif | 2025-08-19
244+
3 | https://media.giphy.com/media/14hs7g86sQqDF6/giphy.gif | 2025-08-19
245+
(3 rows)
246+
```
247+
248+
Hooray! The database is populated and ready to go!
249+
250+
251+
## πŸ’» Updating the app to use the database
252+
253+
1. In order to connect to the PostgreSQL database, we need code that can communicate to the database. Fortunately, we can use the [pg library](https://www.npmjs.com/package/pg). Install it by running the following command:
254+
255+
```bash
256+
npm add pg
257+
```
258+
259+
2. In the `src` folder, create a file named `db.js` with the following contents:
260+
261+
```javascript
262+
const { Pool } = require('pg');
263+
264+
const pool = new Pool({
265+
user: process.env.PGUSER || "postgres",
266+
password: process.env.PGPASSWORD || "secret",
267+
host: process.env.PGHOST || "localhost",
268+
port: process.env.PGPORT || 5432,
269+
database: process.env.PGDATABASE || "postgres"
270+
});
271+
272+
async function getRandomMeme() {
273+
const res = await pool.query('SELECT url FROM memes ORDER BY RANDOM() LIMIT 1');
274+
return res.rows[0]?.url || "https://media.giphy.com/media/8L0Pky6C83SzkzU55a/giphy.gif";
275+
}
276+
277+
module.exports = {
278+
getRandomMeme
279+
};
280+
```
281+
282+
This will provide the code required to connect to the database and get a random meme url.
283+
284+
3. With the database code, we only need to update our website to use it. In the `src/index.js` file, add the following to the top of the file:
285+
286+
```javascript
287+
const { getRandomMeme } = require("./db");
288+
```
289+
290+
This gives us access to the `getRandomMeme` function we defined in the other file.
291+
292+
4. Now, we only need to update our web page to use a meme URL that we get from the database. Update the `memeUrl` section to the following:
293+
294+
```javascript
295+
memeUrl: await getRandomMeme(),
296+
```
297+
298+
5. Refresh your page. You should now see the memes defined in the database! πŸŽ‰
299+
300+
## 🐳 Using Compose to make the services easier to start
301+
302+
Hopefully, you're starting to see how Docker makes it easy to run services. No need to install anything. Very simple configuration. It just works!
303+
304+
But, if your app starts to have quite a few services, telling team members to run a bunch of `docker run` commands is a lot of work.
305+
306+
That's where Docker Compose comes in! With Compose, we can create a `compose.yaml` that defines everything for us.
307+
308+
1. Before we define the Compose file, let's remove the database container we already have running:
309+
310+
```bash
311+
docker rm -f postgres
312+
```
313+
314+
2. At the root of the project, create a file named `compose.yaml` with the following contents:
315+
316+
```yaml
317+
services:
318+
db:
319+
image: postgres:17-alpine
320+
ports:
321+
- 5432:5432
322+
volumes:
323+
- ./db:/docker-entrypoint-initdb.d
324+
environment:
325+
POSTGRES_PASSWORD: secret
326+
```
327+
328+
You should probably recognize this has almost all of the same config from the previous `docker run` commands, but just in a different format.
329+
330+
3. Start the app now by using `docker compose`:
331+
332+
```bash
333+
docker compose up -d
334+
```
335+
336+
The `-d` will run everything in the background. But, you should see output indicating the containers have started:
337+
338+
```plaintext
339+
[+] Running 2/2
340+
βœ” Network project_default Created 0.0s
341+
βœ” Container project-db-1 Started 0.2s
342+
```
343+
344+
4. To prove it's working, run the following commands to delete all of the memes in the database and then add a new one:
345+
346+
```bash
347+
psql -h localhost -U postgres -c "DELETE FROM memes"
348+
```
349+
350+
And add another one into the database:
351+
352+
```bash
353+
psql -h localhost -U postgres -c "INSERT INTO memes (url) VALUES ('https://media.giphy.com/media/l0MYt5jPR6QX5pnqM/giphy.gif')"
354+
```
355+
356+
5. Refresh the browser several times and you should only see a single celebratory meme.
357+
358+
## 🐳 Docker Recap
359+
360+
Before moving on, let's take a step back and focus on what we learned.
361+
362+
- πŸŽ‰ **No install required.** PostgreSQL is running in a container with minimal effort or setup required. Even with database schema setup!
363+
- Docker provides many options to configure and troubleshoot containers
364+
- πŸŽ‰ **Compose makes things easy.** If we add the Compose file to our repo, other team members only need to `git clone` and run `docker compose up`. Everything will be there for them.
365+
- Everyone is on the same version of the database. If a new version comes out, we only need to update the Compose file and everyone will be updated.
366+
367+
368+
## Next steps
369+
370+
Now that you've added a containerized service, let's add one more capability to our dev environment to make it easier for developers... troubleshooting and debugging tools!

β€Ždocs/02-running-a-containerized-service.mdβ€Ž

Whitespace-only changes.

β€Žlabspace.yamlβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ description: |
55
sections:
66
- title: Introduction
77
contentPath: ./docs/01-introduction.md
8-
- title: Running a containerized service
9-
contentPath: ./docs/02-running-a-containerized-service.md
8+
- title: Adding containerized services
9+
contentPath: ./docs/02-adding-containerized-services.md
1010
- title: Adding dev tools
1111
contentPath: ./docs/03-adding-dev-tools.md
1212
- title: Running tests

0 commit comments

Comments
Β (0)