Mohammad-Ali A’RÂBI – Docker https://www.docker.com Tue, 28 Mar 2023 18:03:38 +0000 en-US hourly 1 https://wordpress.org/?v=6.2.2 https://www.docker.com/wp-content/uploads/2023/04/cropped-Docker-favicon-32x32.png Mohammad-Ali A’RÂBI – Docker https://www.docker.com 32 32 Containerizing an Event Posting App Built with the MEAN Stack https://www.docker.com/blog/containerizing-an-event-posting-app-built-with-the-mean-stack/ Tue, 28 Mar 2023 18:03:36 +0000 https://www.docker.com/?p=41248 This article is a result of open source collaboration. During Hacktoberfest 2022, the project was announced in the Black Forest Docker meetup group and received contributions from members of the meetup group and other Hacktoberfest contributors. Almost all of the code in the GitHub repo was written by Stefan Ruf, Himanshu Kandpal, and Sreekesh Iyer.

The MEAN stack is a fast-growing, open source JavaScript stack used to develop web applications. MEAN is a diverse collection of robust technologies — MongoDB, Express.js, Angular, and Node.js — for developing scalable web applications. 

The stack is a popular choice for web developers as it allows them to work with a single language throughout the development process and it also provides a lot of flexibility and scalability. Node, Express, and Angular even claimed top spots as popular frameworks or technologies in Stack Overflow’s 2022 Developer Survey.

In this article, we’ll describe how the MEAN stack works using an Event Posting app as an example.

MEAN stack logos for MongoDB, Express.js, Angular, and Node.js

How does the MEAN stack work?

MEAN consists of the following four components:

  • MongoDB — A NoSQL database 
  • ExpressJS —  A backend web-application framework for NodeJS
  • Angular — A JavaScript-based front-end web development framework for building dynamic, single-page web applications
  • NodeJS — A JavaScript runtime environment that enables running JavaScript code outside the browser, among other things

Here’s a brief overview of how the different components might work together:

  • A user interacts with the frontend, via the web browser, which is built with Angular components. 
  • The backend server delivers frontend content, via ExpressJS running atop NodeJS.
  • Data is fetched from the MongoDB database before it returns to the frontend. Here, your application displays it for the user.
  • Any interaction that causes a data-change request is sent to the Node-based Express server.

Why is the MEAN stack so popular?

The MEAN stack is often used to build full-stack, JavaScript web applications, where the same language is used for both the client-side and server-side of the application. This approach can make development more efficient and consistent and make it easier for developers to work on both the frontend and backend of the application.

The MEAN stack is popular for a few reasons, including the following:

  • Easy learning curve — If you’re familiar with JavaScript and JSON, then it’s easy to get started. MEAN’s structure lets you easily build a three-tier architecture (frontend, backend, database) with just JavaScript and JSON.
  • Model View Architecture — MEAN supports the Model-view-controller architecture, supporting a smooth and seamless development process.
  • Reduces context switching — Because MEAN uses JavaScript for both frontend and backend development, developers don’t need to worry about switching languages. This capability boosts development efficiency.
  • Open source and active community support — The MEAN stack is purely open source. All developers can build robust web applications. Its frameworks improve the coding efficiency and promote faster app development.

Running the Event Posting app

Here are the key components of the Event Posting app:

Deploying the Event Posting app is a fast process. To start, you’ll clone the repository, set up the client and backend, then bring up the application. 

Then, complete the following steps:

git clone https://github.com/dockersamples/events 
cd events/backend
npm install
npm run dev

General flow of the Event Posting app

The flow of information through the Event Posting app is illustrated in Figure 1 and described in the following steps.

Illustration showing the flow of information through components of the Event Posting app, including the browser, frontend container, backend server, and MongoDB.
Figure 1: General flow of the Event Posting app.
  1. A user visits the event posting app’s website on their browser.
  2. AngularJS, the frontend framework, retrieves the necessary HTML, CSS, and JavaScript files from the server and renders the initial view of the website.
  3. When the user wants to view a list of events or create a new event, AngularJS sends an HTTP request to the backend server.
  4. Express.js, the backend web framework, receives the request and processes it. This step includes interacting with the MongoDB database to retrieve or store data and providing an API for the frontend to access the data.
  5. The back-end server sends a response to the frontend, which AngularJS receives and uses to update the view.
  6. When a user creates a new event, AngularJS sends a POST request to the backend server, which Express.js receives and processes. Express.js stores the new event in the MongoDB database.
  7. The backend server sends a confirmation response to the front-end, which AngularJS receives and uses to update the view and display the new event.
  8. Node.js, the JavaScript runtime, handles the server-side logic for the application and allows for real-time updates. This includes running the Express.js server, handling real-time updates using WebSockets, and handling any other server-side tasks.

You can then access Event Posting at http://localhost:80 in your browser (Figure 2):

Screenshot of blue "Add new event" button.
Figure 2: Add a new event.

Select Add New Event to add the details (Figure 3).

Screenshot of dialog box for adding event details, such as name, organizer, and date.
Figure 3: Add event details.

Save the event details to see the final results (Figure 4).

Screenshot of display showing upcoming events, with example Docker events in Berlin and Bangalore.
Figure 4: Display upcoming events.

Why containerize the MEAN stack?

Containerizing the MEAN stack allows for a consistent, portable, and easily scalable environment for the application, as well as improved security and ease of deployment. Containerizing the MEAN stack has several benefits, such as:

  • Consistency: Containerization ensures that the environment for the application is consistent across different development, testing, and production environments. This approach eliminates issues that can arise from differences in the environment, such as different versions of dependencies or configurations.
  • Portability: Containers are designed to be portable, which means that they can be easily moved between different environments. This capability makes it easy to deploy the MEAN stack application to different environments, such as on-premises or in the cloud.
  • Isolation: Containers provide a level of isolation between the application and the host environment. Thus, the application has access only to the resources it needs and does not interfere with other applications running on the same host.
  • Scalability: Containers can be easily scaled up or down depending on the needs of the application, resulting in more efficient use of resources and better performance.

Containerizing your Event Posting app

Docker helps you containerize your MEAN Stack — letting you bundle your complete Event Posting application, runtime, configuration, and operating system-level dependencies. The container then includes everything needed to ship a cross-platform, multi-architecture web application. 

We’ll explore how to run this app within a Docker container using Docker Official Images. To begin, you’ll need to download Docker Desktop and complete the installation process. This step includes the Docker CLI, Docker Compose, and a user-friendly management UI, which will each be useful later on.

Docker uses a Dockerfile to create each image’s layers. Each layer stores important changes stemming from your base image’s standard configuration. Next, we’ll create an empty Dockerfile in the root of our project repository.

Containerizing your Angular frontend

We’ll build a multi-stage Dockerfile to containerize our Angular frontend. 

A Dockerfile is a plain-text file that contains instructions for assembling a Docker container image. When Docker builds our image via the docker build command, it reads these instructions, executes them, and creates a final image. 

With multi-stage builds, a Docker build can use one base image for compilation, packaging, and unit testing. A separate image holds the application’s runtime. This setup makes the final image more secure and shrinks its footprint (because it doesn’t contain development or debugging tools). 

Let’s walk through the process of creating a Dockerfile for our application. First, create the following empty file with the name Dockerfile in the root of your frontend app.

touch Dockerfile

Then you’ll need to define your base image in the Dockerfile file. Here we’ve chosen the stable LTS version of the Node Docker Official Image. This image comes with every tool and package needed to run a Node.js application:

FROM node:lts-alpine AS build

Next, let’s create a directory to house our image’s application code. This acts as the working directory for your application:

WORKDIR /usr/src/app

The following COPY instruction copies the package.json and src file from the host machine to the container image. 

The COPY command takes two parameters. The first tells Docker which file(s) you’d like to copy into the image. The second tells Docker where you want those files to be copied. We’ll copy everything into our working directory called /usr/src/app.

COPY package.json .

COPY package-lock.json .
RUN npm ci

Next, we need to add our source code into the image. We’ll use the COPY command just like we previously did with our package.json file. 

Note: It’s common practice to copy the package.json file separately from the application code when building a Docker image. This step allows Docker to cache the node_modules layer separately from the application code layer, which can significantly speed up the Docker build process and improve the development workflow.

COPY . .

Then, use npm run build to run the build script from package.json:

RUN npm run build

In the next step, we need to specify the second stage of the build that uses an Nginx image as its base and copies the nginx.conf file to the /etc/nginx directory. It also copies the compiled TypeScript code from the build stage to the /usr/share/nginx/html directory.

FROM nginx:stable-alpine

COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=build /usr/src/app/dist/events /usr/share/nginx/html

Finally, the EXPOSE instruction tells Docker which port the container listens on at runtime. You can specify whether the port listens on TCP or UDP. The default is TCP if the protocol isn’t specified.

EXPOSE 80

Here is our complete Dockerfile:

# Builder container to compile typescript
FROM node:lts-alpine AS build
WORKDIR /usr/src/app

# Install dependencies
COPY package.json .
COPY package-lock.json .
RUN npm ci

# Copy the application source
COPY . .
# Build typescript
RUN npm run build



FROM nginx:stable-alpine

COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=build /usr/src/app/dist/events /usr/share/nginx/html

EXPOSE 80

Now, let’s build our image. We’ll run the docker build command as above, but with the -f Dockerfile flag. The -f flag specifies your Dockerfile name. The “.” command will use the current directory as the build context and read a Dockerfile from stdin. The -t tags the resulting image.

docker build . -f Dockerfile -t events-fe:1

Containerizing your Node.js backend

Let’s walk through the process of creating a Dockerfile for our backend as the next step. First, create the following empty Dockerfile in the root of your backend Node app:

# Builder container to compile typescript
FROM node:lts-alpine AS build
WORKDIR /usr/src/app

# Install dependencies
COPY package.json .
COPY package-lock.json .
RUN npm ci

# Copy the application source
COPY . .
# Build typescript
RUN npm run build



FROM node:lts-alpine
WORKDIR /app
COPY package.json .
COPY package-lock.json .
COPY .env.production .env

RUN npm ci --production

COPY --from=build /usr/src/app/dist /app

EXPOSE 8000
CMD [ "node", "src/index.js"]

This Dockerfile is useful for building and running TypeScript applications in a containerized environment, allowing developers to package and distribute their applications more easily.

The first stage of the build process, named build, is based on the official Node.js LTS Alpine Docker image. It sets the working directory to /usr/src/app and copies the package.json and package-lock.json files to install dependencies with the npm ci command. It then copies the entire application source code and builds TypeScript with the npm run build command.

The second stage of the build process, named production, also uses the official Node.js LTS Alpine Docker image. It sets the working directory to /app and copies the package.json, package-lock.json, and .env.production files. It then installs only production dependencies with npm ci --production command, and copies the output of the previous stage, the compiled TypeScript code, from /usr/src/app/dist to /app.

Finally, it exposes port 8000 and runs the command node src/index.js when the container is started.

Defining services using a Compose file

Here’s how our services appear within a Docker Compose file:

services:
  frontend:
    build:
      context: "./frontend/events"
      dockerfile: "./Dockerfile"
    networks:
      - events_net
  backend:
    build:
      context: "./backend"
      dockerfile: "./Dockerfile"
    networks:
      - events_net
  db:
    image: mongo:latest
    ports:
      - 27017:27017
    networks:
      - events_net
  proxy:
    image: nginx:stable-alpine
    environment:
      - NGINX_ENVSUBST_TEMPLATE_SUFFIX=.conf
      - NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx
    volumes:
      - ${PWD}/nginx.conf:/etc/nginx/templates/nginx.conf.conf
    ports:
      - 80:80
    networks:
      - events_net

networks:
  events_net:

Your example application has the following parts:

  • Four services backed by Docker images: Your Angular frontend, Node.js backend, MongoDB database, and Nginx as a proxy server
  • The frontend and backend services are built from Dockerfiles located in ./frontend/events and ./backend directories, respectively. Both services are attached to a network called events_net.
  • The db service is based on the latest version of the MongoDB Docker image and exposes port 27017. It is attached to the same events_net network as the frontend and backend services.
  • The proxy service is based on the stable-alpine version of the Nginx Docker image. It has two environment variables defined, NGINX_ENVSUBST_TEMPLATE_SUFFIX and NGINX_ENVSUBST_OUTPUT_DIR, that enable environment variable substitution in Nginx configuration files. 
  • The proxy service also has a volume defined that maps the local nginx.conf file to /etc/nginx/templates/nginx.conf.conf in the container. Finally, it exposes port 80 and is attached to the events_net network.
  • The events_net network is defined at the end of the file, and all services are attached to it. This setup enables communication between the containers using their service names as hostnames.

You can clone the repository or download the docker-compose.yml file directly from Dockersamples on GitHub.

Bringing up the container services

You can start the MEAN application stack by running the following command:

docker compose up -d 

Next, use the docker compose ps command to confirm that your stack is running properly. Your terminal will produce the following output:

$ docker compose ps   
NAME                IMAGE                 COMMAND                  SERVICE             CREATED             STATUS              PORTS
events-backend-1    events-backend        "docker-entrypoint.s…"   backend             29 minutes ago      Up 29 minutes       8000/tcp
events-db-1         mongo:latest          "docker-entrypoint.s…"   db                  5 seconds ago       Up 4 seconds        0.0.0.0:27017->27017/tcp
events-frontend-1   events-frontend       "/docker-entrypoint.…"   frontend            29 minutes ago      Up 29 minutes       80/tcp
events-proxy-1      nginx:stable-alpine   "/docker-entrypoint.…"   proxy               29 minutes ago      Up 29 minutes       0.0.0.0:80->80/tcp

Viewing the containers via Docker Dashboard

You can also leverage the Docker Dashboard to view your container’s ID and easily access or manage your application (Figure 5):

Screenshot of Docker Dashboard showing running containers.
Figure 5: Viewing running containers in Docker Dashboard.

Conclusion

Congratulations! You’ve successfully learned how to containerize a MEAN-backed Event Posting application with Docker. With a single YAML file, we’ve demonstrated how Docker Compose helps you easily build and deploy your MEAN stack in seconds. With just a few extra steps, you can apply this tutorial while building applications with even greater complexity. Happy developing!

]]>
How to Build and Deploy a URL Shortener Using TypeScript and Nest.js https://www.docker.com/blog/how-to-build-and-deploy-a-url-shortener-using-typescript-and-nest-js/ Tue, 05 Jul 2022 14:00:37 +0000 https://www.docker.com/?p=34553 At Docker, we’re incredibly proud of our vibrant, diverse and creative community. From time to time, we feature cool contributions from the community on our blog to highlight some of the great work our community does. Are you working on something awesome with Docker? Send your contributions to Ajeet Singh Raina (@ajeetraina) on the Docker Community Slack and we might feature your work!

 

Over the last five years, TypeScript’s popularity has surged among enterprise developers. In Stack Overflow’s 2022 Developer Survey, TypeScript ranked third in the “most wanted” category. Stack Overflow reserves this distinction for developers who aren’t developing with a specific language or technology, but have expressed interest in doing so.

 

image6

Data courtesy of Stack Overflow.

 

TypeScript’s incremental adoption is attributable to enhancements in developer code quality and comprehensibility. Overall, Typescript encourages developers to thoroughly document their code and inspires greater confidence through ease of use. TypeScript offers every modern JavaScript feature while introducing powerful concepts like interfaces, unions, and intersection types. It improves developer productivity by clearly displaying syntax errors during compilation, rather than letting things fail at runtime.

However, remember that every programming language comes with certain drawbacks, and TypeScript is no exception. Long compilation times and a steeper learning curve for new JavaScript users are most noteworthy. The good news is that TypeScript’s pros outweigh its cons for ideal use cases. We’ll tackle one of those now. 

Building Your Application

In this tutorial, you’ll learn how to build a basic URL shortener from scratch using TypeScript and Nest.

First, you’ll create a basic application in Nest without using Docker. You’ll see how the application lets you build a simple URL shortening service in Nest and TypeScript, with a Redis backend. Next, you’ll learn how Docker Compose can help you jointly run a Nest.js, TypeScript, and Redis backend to power microservices. Let’s jump in.

Getting Started

The following key components are essential to completing this walkthrough:

 

Before starting, make sure you have Node installed on your system. Then, follow these steps to build a simple web application with TypeScript.

Creating a Nest Project

Nest is currently the fastest growing server-side development framework in the JavaScript ecosystem. It’s ideal for writing scalable, testable, and loosely-coupled applications. Nest provides a level of abstraction above common Node.js frameworks and exposes their APIs to the developer. Under the hood, Nest makes use of robust HTTP server frameworks like Express (the default) and can optionally use Fastify as well! It supports databases like PostgreSQL, MongoDB, and MySQL. NestJS is heavily influenced by Angular, React, and Vue — while offering dependency injection right out of the box.

For first-time users, we recommend creating a new project with the Nest CLI. First, enter the following command to install the Nest CLI.

npm install -g @nestjs/cli

 

Next, let’s create a new Nest.js project directory called backend.

mkdir backend

 

It’s time to populate the directory with the initial core Nest files and supporting modules. From your new backend directory, run Nest’s bootstrapping command. We’ll call our new application link-shortener:

nest new link-shortener

⚡  We will scaffold your app in a few seconds..

CREATE link-shortener/.eslintrc.js (665 bytes)
CREATE link-shortener/.prettierrc (51 bytes)
CREATE link-shortener/README.md (3340 bytes)
CREATE link-shortener/nest-cli.json (118 bytes)
CREATE link-shortener/package.json (1999 bytes)
CREATE link-shortener/tsconfig.build.json (97 bytes)
CREATE link-shortener/tsconfig.json (546 bytes)
CREATE link-shortener/src/app.controller.spec.ts (617 bytes)
CREATE link-shortener/src/app.controller.ts (274 bytes)
CREATE link-shortener/src/app.module.ts (249 bytes)
CREATE link-shortener/src/app.service.ts (142 bytes)
CREATE link-shortener/src/main.ts (208 bytes)
CREATE link-shortener/test/app.e2e-spec.ts (630 bytes)
CREATE link-shortener/test/jest-e2e.json (183 bytes)

? Which package manager would you ❤️  to use? (Use arrow keys)
❯ npm 
  yarn 
  pnpm 

 

All three packages managers are usable, but we’ll choose npm for the purposes of this walkthrough.

Which package manager would you ❤️  to use? npm
✔ Installation in progress... ☕

🚀  Successfully created project link-shortener
👉  Get started with the following commands:

$ cd link-shortener
$ npm run start

                                         
                          Thanks for installing Nest 🙏
                 Please consider donating to our open collective
                        to help us maintain this package.                     
               🍷  Donate: https://opencollective.com/nest

 

Once the command is executed successfully, it creates a new link-shortener project directory with node modules and a few other boilerplate files. It also creates a new src/ directory populated with several core files as shown in the following directory structure:

tree -L 2 -a
.
└── link-shortener
    ├── dist
    ├── .eslintrc.js
    ├── .gitignore
    ├── nest-cli.json
    ├── node_modules
    ├── package.json
    ├── package-lock.json
    ├── .prettierrc
    ├── README.md
    ├── src
    ├── test
    ├── tsconfig.build.json
    └── tsconfig.json

5 directories, 9 files

 

Let’s look at the core files ending with .ts (TypeScript) under /src directory:

src % tree
.
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.ts

0 directories, 5 files

 

Nest embraces modularity. Accordingly, two of the most important Nest app components are controllers and providers. Controllers determine how you handle incoming requests. They’re responsible for accepting incoming requests, performing some kind of operation, and returning the response. Meanwhile, providers are extra classes which you can inject into the controllers or to certain providers. This grants various supplemental functionality. We always recommend reading up on providers and controllers to better understand how they work.

The app.module.ts is the root module of the application and bundles up a couple of controllers and providers that the controller uses.

cat app.module.ts  
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 

As shown in the above file, AppModule is just an empty class. Nest’s @Module decorator is responsible for providing the config that lets Nest build a functional application from it.

First, app.controller.ts exports  a basic controller with a single route. The app.controller.spec.ts is the unit test for the controller. Second, app.service.ts is a basic service with a single method. Third, main.ts is the entry file of the application. It bootstraps the application by calling NestFactory.create, then starts the new application by having it listen for inbound HTTP requests on port 3000.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap(); 

 

Running the Application

Once the installation is completed, run the following command to start your application:

npm run start

> link-shortener@0.0.1 start
> nest start

[Nest] 68686  - 05/31/2022, 5:50:59 PM     LOG [NestFactory] Starting Nest application...
[Nest] 68686  - 05/31/2022, 5:50:59 PM     LOG [InstanceLoader] AppModule dependencies initialized +24ms
[Nest] 68686  - 05/31/2022, 5:50:59 PM     LOG [RoutesResolver] AppController {/}: +4ms
[Nest] 68686  - 05/31/2022, 5:50:59 PM     LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 68686  - 05/31/2022, 5:50:59 PM     LOG [NestApplication] Nest application successfully started +1ms

This command starts the app with the HTTP server listening on the port defined in the src/main.ts file. Once the application is successfully running, open your browser and navigate to http://localhost:3000. You should see the “Hello World!” message:

image5

Add a Repository

A repository is a layer that is in charge of storing stuff. Here, we would want a repository layer to store the mapping between the hashes and their original URLs.

Let’s first create an interface for the repository. Create a file named app.repository.ts and fill it up as follows:

import { Observable } from 'rxjs';

export interface AppRepository {
  put(hash: string, url: string): Observable<string>;
  get(hash: string): Observable<string>;
}

export const AppRepositoryTag = 'AppRepository';
</pre>
&nbsp;
<p class="c9"><span class="c6">Now, let's create a simple repository that stores the mappings in a hashmap in the memory. Create a file named <code>app.repository.hashmap.ts</code>:</span></p>

<pre>
import { AppRepository } from './app.repository';
import { Observable, of } from 'rxjs';

export class AppRepositoryHashmap implements AppRepository {
  private readonly hashMap: Map<string, string>;

  constructor() {
    this.hashMap = new Map<string, string>();
  }

  get(hash: string): Observable<string> {
    return of(this.hashMap.get(hash));
  }

  put(hash: string, url: string): Observable<string> {
    return of(this.hashMap.set(hash, url).get(hash));
  }
}

Now, let’s instruct Nest.js that if one asked for AppRepositoryTag provide them with AppRepositoryHashMap. First, let’s do it in the app.module.ts:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AppRepositoryTag } from './app.repository';
import { AppRepositoryHashmap } from './app.repository.hashmap';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    { provide: AppRepositoryTag, useClass: AppRepositoryHashmap }, // <-- here
  ],
})
export class AppModule {}

Let’s now add a new test for our new endpoint in app.service.spec.ts:

import { Test, TestingModule } from "@nestjs/testing";
import { AppService } from "./app.service";
import { AppRepositoryTag } from "./app.repository";
import { AppRepositoryHashmap } from "./app.repository.hashmap";
import { mergeMap, tap } from "rxjs";

describe('AppService', () => {
  let appService: AppService;

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      providers: [
        { provide: AppRepositoryTag, useClass: AppRepositoryHashmap },
        AppService,
      ],
    }).compile();

    appService = app.get<AppService>(AppService);
  });

  describe('retrieve', () => {
    it('should retrieve the saved URL', done => {
      const url = 'docker.com';
      appService.shorten(url)
        .pipe(mergeMap(hash => appService.retrieve(hash)))
        .pipe(tap(retrieved => expect(retrieved).toEqual(url)))
        .subscribe({ complete: done })
    });
  });
});

 

Before running our tests, let’s implement the function in app.service.ts:

import { Inject, Injectable } from '@nestjs/common';
import { map, Observable } from 'rxjs';
import { AppRepository, AppRepositoryTag } from './app.repository';

@Injectable()
export class AppService {
  constructor(
    @Inject(AppRepositoryTag) private readonly appRepository: AppRepository,
  ) {}

  getHello(): string {
    return 'Hello World!';
  }

  shorten(url: string): Observable<string> {
    const hash = Math.random().toString(36).slice(7);
    return this.appRepository.put(hash, url).pipe(map(() => hash)); // <-- here
  }

  retrieve(hash: string): Observable<string> {
    return this.appRepository.get(hash); // <-- and here
  }
}

Run these tests once more to confirm that everything passes, before we begin storing the data in a real database.

Add a Database

So far, we’re just storing our mappings in memory. That’s fine for testing, but we’ll need to store them somewhere more centralized and durable in production. We’ll use Redis, a popular key-value store available on Docker Hub.

Let’s install this Redis client by running the following command from the backend/link-shortener directory:

npm install redis@4.1.0 --save

 

Inside /src, create a new version of the AppRepository interface that uses Redis. We’ll call this file app.repository.redis.ts:

import { AppRepository } from './app.repository';
import { Observable, from, mergeMap } from 'rxjs';
import { createClient, RedisClientType } from 'redis';

export class AppRepositoryRedis implements AppRepository {
  private readonly redisClient: RedisClientType;

  constructor() {
    const host = process.env.REDIS_HOST || 'redis';
    const port = +process.env.REDIS_PORT || 6379;
    this.redisClient = createClient({
      url: `redis://${host}:${port}`,
    });
    from(this.redisClient.connect()).subscribe({ error: console.error });
    this.redisClient.on('connect', () => console.log('Redis connected'));
    this.redisClient.on('error', console.error);
  }

  get(hash: string): Observable<string> {
    return from(this.redisClient.get(hash));
  }

  put(hash: string, url: string): Observable<string> {
    return from(this.redisClient.set(hash, url)).pipe(
            mergeMap(() => from(this.redisClient.get(hash))),
    );
  }
}

 

Finally, it’s time to change the provider in app.module.ts to our new Redis repository from the in-memory version:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AppRepositoryTag } from './app.repository';
import { AppRepositoryRedis } from "./app.repository.redis";

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    { provide: AppRepositoryTag, useClass: AppRepositoryRedis }, // <-- here
  ],
})
export class AppModule {}

 

Finalize the Backend

Head back to app.controller.ts and create another endpoint for redirect:

import { Body, Controller, Get, Param, Post, Redirect } from '@nestjs/common';
import { AppService } from './app.service';
import { map, Observable, of } from 'rxjs';

interface ShortenResponse {
  hash: string;
}

interface ErrorResponse {
  error: string;
  code: number;
}

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Post('shorten')
  shorten(@Body('url') url: string): Observable<ShortenResponse | ErrorResponse> {
    if (!url) {
      return of({ error: `No url provided. Please provide in the body. E.g. {'url':'https://google.com'}`, code: 400 });
    }
    return this.appService.shorten(url).pipe(map(hash => ({ hash })));
  }

  @Get(':hash')
  @Redirect()
  retrieveAndRedirect(@Param('hash') hash): Observable<{ url: string }> {
    return this.appService.retrieve(hash).pipe(map(url => ({ url })));
  }
}

 

Click here to access the code previously developed for this example.

Containerizing the TypeScript Application

Docker helps you containerize your TypeScript app, letting you bundle together your complete TypeScript application, runtime, configuration, and OS-level dependencies. This includes everything needed to ship a cross-platform, multi-architecture web application. 

Let’s see how you can easily run this app inside a Docker container using a Docker Official Image. First, you’ll need to download Docker Desktop. Docker Desktop accelerates the image-building process while making useful images more discoverable. Complete the installation process once your download is finished.

Docker uses a Dockerfile to specify an image’s “layers.” Each layer stores important changes building upon the base image’s standard configuration. Create the following empty Dockerfile in your Nest project.

 

touch Dockerfile

 

Use your favorite text editor to open this Dockerfile. You’ll then need to define your base image. Let’s also quickly create a directory to house our image’s application code. This acts as the working directory for your application:

 

WORKDIR /app

 

The following COPY instruction copies the files from the host machine to the container image:

 

COPY . .

 

Finally, this closing line tells Docker to compile and run your application packages:

 

CMD["npm", "run", "start:dev"]

 

Here’s your complete Dockerfile:

FROM node:16
COPY . .
WORKDIR /app
RUN npm install
EXPOSE 3000
CMD ["npm", "run", "start:dev"]

 

You’ve effectively learned how to build a Dockerfile for a sample TypeScript app. Next, let’s see how to create an associated Docker Compose file for this application. Docker Compose is a tool for defining and running multi-container Docker applications. With Compose, you’ll use a YAML file to configure your services. Then, with a single command, you can create and start every service from your configuration.

Defining Services Using a Compose File

It’s time to define your services in a Docker Compose file:

services:
  redis:
    image: 'redis/redis-stack'
    ports:
      - '6379:6379'
      - '8001:8001'
    networks:
      - urlnet
  dev:
    build:
      context: ./backend/link-shortener
      dockerfile: Dockerfile
    environment:
      REDIS_HOST: redis
      REDIS_PORT: 6379
    ports:
      - '3000:3000'
    volumes:
      - './backend/link-shortener:/app'
    depends_on:
      - redis
    networks:
      - urlnet

networks:
  urlnet:

 

Your example application has the following parts:

  • Two services backed by Docker images: your frontend dev app and your backend database redis
  • The redis/redis-stack Docker image is an extension of Redis that adds modern data models and processing engines to provide a complete developer experience. We use port 8001 for RedisInsight — a visualization tool for understanding and optimizing Redis data.
  • The frontend, accessible via port 3000
  • The depends_on parameter, letting you create your backend service before the frontend service starts
  • One persistent volume, attached to the backend
  • The environmental variables for your Redis database

 

Once you’ve stopped the frontend and backend services that we ran in the previous section, let’s build and start our services using the docker-compose up command:

docker compose up -d –build

 

Note: If you’re using Docker Compose v1, the command line syntax is docker-compose with a hyphen. If you’re using v2, which ships with Docker Desktop, the hyphen is omitted and docker compose is correct. 

docker compose ps
NAME                        COMMAND                  SERVICE             STATUS              PORTS
link-shortener-js-dev-1     "docker-entrypoint.s…"   dev                 running             0.0.0.0:3000-&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;3000/tcp
link-shortener-js-redis-1   "/entrypoint.sh"         redis               running             0.0.0.0:6379-&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;6379/tcp, 0.0.0.0:8001-&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;8001/tcp

 

Just like that, you’ve created and deployed your TypeScript URL shortener! You can use this in your browser like before. If you visit the application at https://localhost:3000, you should see a friendly “Hello World!” message. Use the following curl command to shorten a new link:

curl -XPOST -d "url=https://docker.com" localhost:3000/shorten

 

Here’s your response:

{"hash":"l6r71d"}

 

This hash may differ on your machine. You can use it to redirect to the original link. Open any web browser and visit https://localhost:3000/l6r71d to access Docker’s website.

Viewing the Redis Keys

You can view the Redis keys with the RedisInsight tool by visiting https://localhost:8001.

 

image1

Viewing the Compose Logs

You can use docker compose logs -f to check and view your Compose logs:

[6:17:19 AM] Starting compilation in watch mode...
link-shortener-js-dev-1    | 
link-shortener-js-dev-1    | [6:17:22 AM] Found 0 errors. Watching for file changes.
link-shortener-js-dev-1    | 
link-shortener-js-dev-1    | [Nest] 31  - 06/18/2022, 6:17:23 AM     LOG [NestFactory] Starting Nest application...
link-shortener-js-dev-1    | [Nest] 31  - 06/18/2022, 6:17:23 AM     LOG [InstanceLoader] AppModule dependencies initialized +21ms
link-shortener-js-dev-1    | [Nest] 31  - 06/18/2022, 6:17:23 AM     LOG [RoutesResolver] AppController {/}: +3ms
link-shortener-js-dev-1    | [Nest] 31  - 06/18/2022, 6:17:23 AM     LOG [RouterExplorer] Mapped {/, GET} route +1ms
link-shortener-js-dev-1    | [Nest] 31  - 06/18/2022, 6:17:23 AM     LOG [RouterExplorer] Mapped {/shorten, POST} route +0ms
link-shortener-js-dev-1    | [Nest] 31  - 06/18/2022, 6:17:23 AM     LOG [RouterExplorer] Mapped {/:hash, GET} route +1ms
link-shortener-js-dev-1    | [Nest] 31  - 06/18/2022, 6:17:23 AM     LOG [NestApplication] Nest application successfully started +1ms
link-shortener-js-dev-1    | Redis connected

You can also leverage the Docker Dashboard to view your container’s ID and easily access or manage your application:

 

image3

 

You can also inspect important logs via the Docker Dashboard:

 

image2

Conclusion

Congratulations! You’ve successfully learned how to build and deploy a URL shortener with TypeScript and Nest. Using a single YAML file, we demonstrated how Docker Compose helps you easily build and deploy a TypeScript-based URL shortener app in seconds. With just a few extra steps, you can apply this tutorial while building applications with much greater complexity.

Happy coding.

]]>
How to Build and Deploy a Django-based URL Shortener App from Scratch https://www.docker.com/blog/how-to-build-and-deploy-a-django-based-url-shortener-app-from-scratch/ Tue, 07 Jun 2022 15:00:48 +0000 https://www.docker.com/?p=33930 At Docker, we are incredibly proud of our vibrant, diverse and creative community. From time to time, we feature cool contributions from the community on our blog to highlight some of the great work our community does. Are you working on something awesome with Docker? Send your contributions to Ajeet Singh Raina (@ajeetraina) on the Docker Community Slack and we might feature your work!

URL shortening is a widely adopted technique that’s used to create short, condensed, and unique aliases for long URL links. Websites like tinyurl.com, bit.ly and ow.ly offer online URL shortener services, while social media sites integrate shorteners right into their product, like Twitter with its use of t.co. This is especially important for Twitter, where shortened links allow users to share long URLs in a Tweet while still fitting in the maximum number of characters for their message.

Why are URL shortener techniques so popular? First, the URL shortener technique allows you to create a short URL that is easy to remember and manage. Say, if you have a brand name, a short URL just consisting of a snippet of your company name is easier to identify and remember.

Screenshot 2022 06 06 at 8.50.51 AM

Second, oversized and hard-to-guess URLs might sometimes look too suspicious and clunky. Imagine a website URL link that has UTM parameters embedded. UTMs are snippets of text added to the end of a URL to help marketers track where website traffic comes from if users click a link to this URL. With too many letters, backslashes and question marks, a long URL might look insecure. Some users might still think that there is a security risk involved with a shortened URL as you cannot tell where you’re going to land, but there are services like Preview mode that allows you to see a preview version of long URL before it instantly redirects you to the actual site.

How do they actually work? Whenever a user clicks a link (say, https://tinyurl.com/2p92vwuh), an HTTP request is sent to the backend server with the full URL. The backend server reads the path part(2p92vwuh) that maps to the database that stores a description, name, and the real URL. Then it issues a redirect, which is an HTTP 302 response with the target URL in the header.

Screenshot 2022 06 07 at 11.22.25 AM

Building the application

In this blog tutorial, you’ll learn how to build a basic URL shortener using Python and Django.

First, you’ll create a basic application in Python without using Docker. You’ll see how the application lets you shorten a URL. Next, you’ll build a Docker image for that application. You’ll also learn how Docker Compose can help you rapidly deploy your application within containers. Let’s jump in.

Key Components

Here’s what you’ll need to use for this tutorial:

Getting Started

Once you have Python 3.8+ installed on your system, follow these steps to build a basic URL shortener clone from scratch.

Step 1. Create a Python virtual environment

Virtualenv is a tool for creating isolated virtual python environments. It’s a self-contained directory tree that contains a Python installation from a particular version of Python, as well as a number of additional packages.

The venv module is used to create and manage virtual environments. In most of the cases, venv is usually the most recent version of Python. If you have multiple versions of Python, you can create a specific Python version.

Use this command to create a Python virtual environment to install packages locally

mkdir -p venv
python3 -m venv venv

The above command will create a directory if it doesn’t exist and also create sub-directories that contain a copy of the Python interpreter and a number of supporting files.

$ tree venv -L 2
venv
├── bin
│   ├── Activate.ps1
│   ├── activate
│   ├── activate.csh
│   ├── activate.fish
│   ├── easy_install
│   ├── easy_install-3.8
│   ├── pip
│   ├── pip3
│   ├── pip3.8
│   ├── python -> python3
│   └── python3 -> /usr/bin/python3
├── include
├── lib
│   └── python3.8
├── lib64 -> lib
└── pyvenv.cfg

5 directories, 12 files

Once you’ve created a virtual environment, you’ll need to activate it:

source ./venv/bin/activate

Step 2. Install Django

The easiest way to install Django is to use the standalone pip installer. PIP(Preferred Installer Program) is the most popular package installer for Python and is a command-line utility that helps you to manage your Python 3rd-party packages. Use the following command to update the pip package and then install Django:

pip install -U pip
pip install Django

You’ll see the following results:

Collecting django
  Downloading Django-4.0.4-py3-none-any.whl (8.0 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.0/8.0 MB 15.9 MB/s eta 0:00:00
Collecting asgiref<4,>=3.4.1
  Downloading asgiref-3.5.2-py3-none-any.whl (22 kB)
Collecting sqlparse>=0.2.2
  Downloading sqlparse-0.4.2-py3-none-any.whl (42 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 42.3/42.3 kB 1.7 MB/s eta 0:00:00
Collecting backports.zoneinfo
  Downloading backports.zoneinfo-0.2.1.tar.gz (74 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 74.1/74.1 kB 3.0 MB/s eta 0:00:00
  Installing build dependencies ... done
   …..

Step 3. Create a Django project

The django-admin is Django’s command-line utility for administrative tasks. The utility helps you automatically create manage.py in each Django project.

mkdir -p src/ && cd src
django-admin startproject url shortener

Django Project Structure:

$ tree urlshortener/
urlshortener/
├── manage.py
└── urlshortener
    ├── __init__.py
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

1 directory, 6 files

In this directory tree:

  • manage.py is Django’s CLI
  • settings.py is where all of the global Django project’s settings reside
  • urls.py is where all the URL mappings reside
  • wsgi.py is an entry-point for WSGI-compatible servers to serve the project in production

Step 4. Creating a Django app for shortening the URL

Change directory to src/urlshortener and run the following command:

cd src/urlshortener
python manage.py startapp main

It will create a new subdirectory called “main” under src/urlshortener as shown below:

src
└── urlshortener
    ├── main
    │   ├── admin.py
    │   ├── apps.py
    │   ├── __init__.py
    │   ├── migrations
    │   ├── models.py
    │   ├── tests.py
    │   └── views.py
    ├── manage.py
    └── urlshortener

In this directory tree:

  • admin.py is where Django’s built-in admin configuration resides
  • migrations is where all of the database migrations reside
  • models.py is where all of the database models for this Django app exist
  • tests.py is self-explanatory
  • views.py is where “controller” functions reside, the functions that are in charge of creating the views

For this tutorial, you’ll only leverage the last one.

Step 5. Create the URL Shortener

pyshorteners is a simple URL shortening API wrapper Python library. With pyshorteners , you can generate a short url or expand another one is as easy as typing

Run the following command to install the package pyshorteners:

pip install pyshorteners

Run the following command to save all your python libraries with current version into requirements.txt file:

pip freeze > requirements.txt

Once the command is successfully run, the requirements.txt gets created with the following entries:

asgiref==3.5.2
backports.zoneinfo==0.2.1
certifi==2022.5.18.1
charset-normalizer==2.0.12
Django==4.0.5
idna==3.3
pyshorteners==1.0.1
requests==2.27.1
sqlparse==0.4.2
urllib3==1.26.9

Head to main/views.py and edit it accordingly:

from django.shortcuts import render
from django.http import HttpResponse
import pyshorteners


# Create your views here.
def shorten(request, url):
    shortener = pyshorteners.Shortener()
    shortened_url = shortener.chilpit.short(url)
    return HttpResponse(f'Shortened URL: <a href="{shortened_url}">{shortened_url}</a>')

In this code listing:

  • In line 1, the render function is imported by default. You won’t remove it now, as you’re going to use it later.
  • In line 2, you’ve imported the class name HttpResponse. This is the type returned with an HTML text.
  • In line 3, the library pyshorteners is imported, which you use to shorten the given URLs.
  • In line 7, the function gets two parameters; a request that is mandatory, and a url that is set by Django. We’ll get to it in the next file.
  • In line 8, you initialized the shortener object.
  • In line 9, the shortened URL is generated by sending a request to chilp.it.
  • In line 10, the shortened URL is returned as a minimal HTML link.

Next, let’s assign a URL to this function.

Create a urls.py under main:

touch main/urls.py

Add the below code:

from django.urls import path

from . import views

urlpatterns = [
    path('shorten/<str:url>', views.shorten, name='shorten'),
]

The URL mapping specifies which function to use and which path parameters there are. In this case, the URL is mapped to the function shorten and with a string parameter named url.

Now head back to the urlshortener/ directory and include the newly created urls.py file:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('', include('main.urls')),
    path('admin/', admin.site.urls),
]

Now, run the development server:

python manage.py runserver

Open http://127.0.0.1:8000/shorten/google.com in your browser and type Enter. It will show you a shortened URL as shown in the following screenshot.

Screenshot 2022 06 06 at 10.27.32 AM

Step 6. Creating the form

In this section, you’ll see how to create a landing page.

mkdir -p main/templates/main
touch main/templates/main/index.html

Open the index.html and fill it up the with following content:

<form action="{% url 'main:shorten_post' %}" method="post">
{% csrf_token %}
<fieldset>
    <input type="text" name="url">
</fieldset>
<input type="submit" value="Shorten">
</form>

In this file:

  • The form action which the URL form sends the request to, is defined by Django’s template tag url. The tag in use is the one created in the URL mappings. Here, the URL tag main:shorten_post doesn’t exist yet. You’ll create it later.
  • The CSRF token is a Django security measure that works out-of-the-box.

Head over to main/views.py under the project directory src/urlshortener/ and add two functions, namely index and shorten_post at the end of the file.

from django.shortcuts import render
from django.http import HttpResponse
import pyshorteners


def index(request):
    return render(request, 'main/index.html')


def shorten_post(request):
    return shorten(request, request.POST['url'])


. . .

Here,

  • The function index renders the HTML template created in the previous step, using the render function.
  • The function shorten_post is a function created to be used for the post requests. The reason for its creation (and not using the previous function) is because Django’s URL mapping only works with path parameters and not post request parameters. So, here, the parameter url is read from the post request and passed to the previously available shorten function.

Now go to the main/urls.py to bind the functions to URLs:

from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('shorten', views.shorten_post, name='shorten_post'),
    path('shorten/<str:url>', views.shorten, name='shorten'),
]

Next, head over to urlshortener/settings.py under src/urlshortener/urlshortener directory and add 'main.apps.MainConfig' to the beginning of the list INSTALLED_APPS:

. . .

INSTALLED_APPS = [
    'main.apps.MainConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

. . .

Step 7. Creating a Database Models

Now, to save the URLs and their short versions locally, you should create database models for them. Head to main/models.py under src/urlshortener/main and create the following model:

from django.db import models


# Create your models here.
class Question(models.Model):
    original_url = models.CharField(max_length=256)
    hash = models.CharField(max_length=10)
    creation_date = models.DateTimeField('creation date')

We’ll assume that the given URLs fit in 256 characters and the short version are less than 10 characters (usually 7 characters would suffice).

Now, create the database migrations:

python manage.py makemigrations

It will show the following results:

Migrations for 'main':
  main/migrations/0001_initial.py
    - Create model Question

A new file will be created under main/migrations.

main % tree migrations 
migrations
├── 0001_initial.py
├── __init__.py
└── __pycache__
    └── __init__.cpython-39.pyc

1 directory, 3 files

Now to apply the database migrations to the default SQLite DB, run:

python manage.py migrate

It shows the following results:

urlshortener % python3 manage.py migrate    
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, main, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying main.0001_initial... OK
  Applying sessions.0001_initial... OK

Now that you have the database models, it’s time to create a shortener service. Create a Python file main/service.py and add the following functionality:

import random
import string
from django.utils import timezone

from .models import LinkMapping


def shorten(url):
    random_hash = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(7))
    mapping = LinkMapping(original_url=url, hash=random_hash, creation_date=timezone.now())
    mapping.save()
    return random_hash


def load_url(url_hash):
    return LinkMapping.objects.get(hash=url_hash)

In this file, in the function shorten, you create a random 7-letter hash, assign the entered URL to this hash, save it into the database, and finally return the hash.

In load_url, you load the original URL from the given hash.

Now, create a new function in the views.py for redirecting:

from django.shortcuts import render, redirect

from . import service

. . .

def redirect_hash(request, url_hash):
    original_url = service.load_url(url_hash).original_url
    return redirect(original_url)

Then create a URL mapping for the redirect function:

urlpatterns = [
    path('', views.index, name='index'),
    path('shorten', views.shorten_post, name='shorten_post'),
    path('shorten/<str:url>', views.shorten, name='shorten'),
    path('<str:url_hash>', views.redirect_hash, name='redirect'),
]

You create a URL mapping for the hashes directly under the main host, e.g. example.com/xDk8vdX. If you want to give it an indirect mapping, like example.com/r/xDk8vdX, then the shortened URL will be longer.

The only thing you have to be careful about is the other mapping example.com/shorten. We made this about the redirect mapping, as otherwise it would’ve resolved to redirect as well.

The final step would be changing the shorten view function to use the internal service:

from django.shortcuts import render, redirect
from django.http import HttpResponse
from django.urls import reverse

from . import service

. . .

def shorten(request, url):
    shortened_url_hash = service.shorten(url)
    shortened_url = request.build_absolute_uri(reverse('redirect', args=[shortened_url_hash]))
    return HttpResponse(f'Shortened URL: <a href="{shortened_url}">{shortened_url}</a>')

You can also remove the third-party shortener library from requirements.txt, as you won’t use it anymore.

Using PostgreSQL

To use PostgreSQL instead of SQLite, you change the config in settings.py:

import os

. . .

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

if os.environ.get('POSTGRES_NAME'):
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.postgresql',
            'NAME': os.environ.get('POSTGRES_NAME'),
            'USER': os.environ.get('POSTGRES_USER'),
            'PASSWORD': os.environ.get('POSTGRES_PASSWORD'),
            'HOST': 'db',
            'PORT': 5432,
        }
    }

The if statement means it only uses the PostgreSQL configuration if it exists in the environment variables. If not set, Django will keep using the SQLite config.

Create a base.html under main/templates/main:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Link Shortener</title>
  <link href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css" rel="stylesheet">
  <script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script>
</head>
<style>
  #main-card {
      margin:0 auto;
      display: flex;
      width: 50em;
      align-items: center;
  }
</style>
<body class="mdc-typography">
<div id="main-card">
  {% block content %}
  {% endblock %}
</div>
</body>

Alter the index.html to use material design:

{% extends 'main/base.html' %}

{% block content %}
<form action="{% url 'shorten_post' %}" method="post">
  {% csrf_token %}
  <label class="mdc-text-field mdc-text-field--outlined">
      <span class="mdc-notched-outline">
        <span class="mdc-notched-outline__leading"></span>
        <span class="mdc-notched-outline__notch">
          <span class="mdc-floating-label" id="my-label-id">URL</span>
        </span>
        <span class="mdc-notched-outline__trailing"></span>
      </span>
    <input type="text" name="url" class="mdc-text-field__input" aria-labelledby="my-label-id">
  </label>
  <button class="mdc-button mdc-button--outlined" type="submit">
    <span class="mdc-button__ripple"></span>
    <span class="mdc-button__label">Shorten</span>
  </button>
</form>
{% endblock %}

Create another view for the response, namely link.html:

{% extends 'main/base.html' %}

{% block content %}
<div class="mdc-card__content">
  <p>Shortened URL: <a href="{{shortened_url}}">{{shortened_url}}</a></p>
</div>
{% endblock %}

Now, get back to views.py and change the shorten function to render instead of returning a plain HTML:

. . .

def shorten(request, url):
    shortened_url_hash = service.shorten(url)
    shortened_url = request.build_absolute_uri(reverse('redirect', args=[shortened_url_hash]))
    return render(request, 'main/link.html', {'shortened_url': shortened_url})

Click here to access the code previously developed for this example. You can directly clone the repository and try executing the following commands to bring up the application.

git clone https://github.com/aerabi/link-shortener
cd link-shortener/src/urlshortener
python manage.py migrate
python manage.py runserver

Screenshot 2022 06 06 at 4.06.28 PM

Step 8. Containerizing the Django App

Docker helps you containerize your Django app, letting you bundle together your complete Django application, runtime, configuration, and OS-level dependencies. This includes everything needed to ship a cross-platform, multi-architecture web application.

Let’s look at how you can easily run this app inside a Docker container using a Docker Official Image. First, you’ll need to download Docker Desktop. Docker Desktop accelerates the image-building process while making useful images more discoverable. Complete the installation process once your download is finished.

You’ve effectively learned how to build a sample Django app. Next, let’s see how to create an associated Docker image for this application.

Docker uses a Dockerfile to specify each image’s “layers.” Each layer stores important changes stemming from the base image’s standard configuration. Create the following empty Dockerfile in your Django project.

touch Dockerfile

Use your favorite text editor to open this Dockerfile. You’ll then need to define your base image.

Whenever you’re creating a Docker image to run a Python program, it’s always recommended to use a smaller base image that helps in speeding up the build process and launching containers at a faster pace.

FROM python:3.9

Next, let’s quickly create a directory to house our image’s application code. This acts as the working directory for your application

RUN mkdir /code
WORKDIR /code

It’s always recommended to update all the packages using the pip command.

RUN pip install --upgrade pip

The following COPY instruction copies the requirements.txt file from the host machine to the container image and stores it under /code directory.

COPY requirements.txt /code/
RUN pip install -r requirements.txt

Next, you need to copy all the directories of the Django project. It includes Django source code and pre-environment configuration files of the artifact.

COPY . /code/

Next, use the EXPOSE instruction to inform Docker that the container listens on the specified network ports at runtime. The EXPOSE instruction doesn’t actually publish the port. It functions as a type of documentation between the person who builds the image and the person who runs the container, about which ports are intended to be published.

EXPOSE 8000

Finally, in the last line of the Dockerfile, specify CMD so as to provide defaults for an executing container. These defaults include Python executables. The runserver command is a built-in subcommand of Django’s manage.py file that will start up a development server for this specific Django project.

CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

Here’s your complete Dockerfile:

FROM python:3.9


RUN mkdir /code
WORKDIR /code
RUN pip install --upgrade pip
COPY requirements.txt /code/

RUN pip install -r requirements.txt
COPY . /code/

EXPOSE 8000

CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

Step 9. Building Your Docker Image

Next, you’ll need to build your Docker image. Enter the following command to kickstart this process, which produces an output soon after:

docker build -t urlshortener .

Step 10. Run Your Django Docker Container

Docker runs processes in isolated containers. A container is a process that runs on a host, which it’s either local or remote. When an operator executes docker run, the container process that runs is isolated with its own file system, networking, and separate process tree from the host.

The following docker run command first creates a writeable container layer over the specified image, and then starts it.

docker run -p 8000:8000 -t urlshortener

Step 11. Running URL Shortener app using Docker Compose

Finally, it’s time to create a Docker Compose file. This single YAML file lets you specify your frontend app and your PostgreSQL database:

services:
  web:
    build:
      context: ./src/urlshortener/
      dockerfile: Dockerfile
    command: gunicorn urlshortener.wsgi:application --bind 0.0.0.0:8000
    ports:
      - 8000:8000
    environment:
      - POSTGRES_NAME=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    depends_on:
      - db
  db:
    image: postgres
    volumes:
      - postgresdb:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres

volumes:
  postgresdb:


Your example application has the following parts:

  • Two services backed by Docker images: your frontend web app and your backend database
  • The frontend, accessible via port 8000
  • The depends_on parameter, letting you create the backend service before the frontend service starts
  • One persistent volume, attached to the backend
  • The environmental variables for your PostgreSQL database

You’ll then start your services using the docker-compose up command.

docker-compose up -d -—build

Note: If you’re using Docker Compose v1, the command line name is docker-compose, with a hyphen. If you’re using v2, which is shipped with Docker Desktop, you should omit the hyphen: docker compose.

docker-compose ps
NAME                   COMMAND                  SERVICE             STATUS              PORTS
link-shortener-db-1    "docker-entrypoint.s…"   db                  running             5432/tcp
link-shortener-web-1   "gunicorn urlshorten…"   web                 running             0.0.0.0:8000->8000/tcp

Now, it’s time to perform the migration:

docker-compose exec web python manage.py migrate

Just like that, you’ve created and deployed your Django URL-shortener app! This is usable in your browser, like before:

Screenshot 2022 06 06 at 6.03.46 PM

You can get the shortened URL by adding the URL as shown below:

Screenshot 2022 06 06 at 6.04.54 PM

Conclusion

Docker helps accelerate the process of building, running, and sharing modern applications. Docker Official Images help you develop your own unique applications, no matter what tech stack you’re accustomed to. With a single YAML file, we demonstrated how Docker Compose helps you easily build and deploy a Django-based URL shortener app in a matter of seconds. With just a few extra steps, you can apply this tutorial while building applications with much greater complexity.

Happy coding.

]]>