|
| 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! |
0 commit comments