Building a Full-Stack Simple Contact Form: Integrating API, Web Interface, CMS, and Database with Docker Compose Multi-Container Architecture

Hey, Let's Build Something Awesome!

Ready to dive into an exciting project? We're going to build a simple yet powerful contact form application from scratch.

This isn't just any contact form, it's a full-stack application that includes an API, a web interface, a CMS, and a database, all working together seamlessly.

And the best part? We'll orchestrate everything using Docker Compose in a multi-container setup.

The Game Plan

Our project is split into three parts. Before we assemble everything, we'll build each component individually:

Part 1: Build the API

Part 2: Develop the Web Interface

Part 3: Set Up the CMS

Important: Before proceeding, make sure you've completed each part. Each component has its own Docker setup and runs independently at this stage.

Now, Let's Assemble the Puzzle

You've built each piece of the puzzle—now it's time to bring them together into a cohesive application using Docker Compose.

Step 1: Ensure You Have All Parts Ready

Step 2: Set Up the Project Structure

Organize your project directory as follows:

📂 contact-form-project/
├── 📂 app/
│   ├── 📂 api-contact-form/
│   ├── 📂 client-contact-form/
│   ├── 📂 cms-contact-form/
├── 📄 docker-compose.yml
├── 📄 .env

Step 3: Aggregate Docker Compose

Docker Compose allows us to define and run multi-container Docker applications. We can configure all our services (API, web interface, CMS, database) in a single docker-compose.yml file, making it easy to manage and run everything together.

The docker-compose.yml file

Here's the Docker Compose configuration we'll use to assemble our services:

services:
  # MariaDB Service
  mariadb-contact-form:
    image: mariadb:latest
    container_name: mariadb-contact-form
    restart: on-failure
    env_file:
      - .env
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
    # ports:
    #   - "${HOST_MARIADB_PORT}:${CONT_MARIADB_PORT}"
    volumes:
      - mariadb-contact-form-data:/var/lib/mysql
    networks:
      - contact-form-network-database

  # PHPMyAdmin Service
  phpmyadmin-contact-form:
    image: phpmyadmin/phpmyadmin:latest
    container_name: phpmyadmin-contact-form
    restart: on-failure
    environment:
      - PMA_HOST=mariadb-contact-form
      - PMA_PORT=${CONT_MARIADB_PORT}
    env_file:
      - .env
    ports:
      - "${HOST_PHPMYADMIN_PORT}:${CONT_PHPMYADMIN_PORT}"
    depends_on:
      - mariadb-contact-form
    networks:
      - contact-form-network-database

  # Contact Form API Service
  api-contact-form:
    build:
      context: ./app/api-contact-form
      dockerfile: Dockerfile
    image: api-contact-form:1.0.0
    container_name: api-contact-form
    restart: on-failure
    depends_on:
      - mariadb-contact-form
    env_file:
      - .env
    ports:
      - "${HOST_API_PORT}:${CONT_API_PORT}"
    environment:
      - APP_PORT=${CONT_API_PORT}
      - APP_TIMEZONE=Asia/Jakarta
      - DB_HOST=mariadb-contact-form
      - DB_PORT=${CONT_MARIADB_PORT}
      - DB_USER=${MYSQL_USER}
      - DB_PASSWORD=${MYSQL_PASSWORD}
      - DB_NAME=${MYSQL_DATABASE}
      - CORS_ALLOWED_ORIGINS=http://localhost:8081,http://localhost:8082,http://cms-contact-form:8081,http://client-contact-form:8082
      - CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
      - CORS_ALLOWED_HEADERS=Origin,Content-Type,Accept,Authorization
      - CORS_ALLOW_CREDENTIALS=true
      - CORS_EXPOSE_HEADERS=Content-Length,Content-Type
    networks:
      - contact-form-network-database
      - contact-form-network-api
  
  # Contact Form Web, Embed UI for submiting contact form from client side
  client-contact-form:
    build:
      context: ./app/client-contact-form
      dockerfile: Dockerfile
    image: client-contact-form:1.0.0
    container_name: client-contact-form
    restart: on-failure
    ports:
      - "${HOST_CLIENT_PORT}:${CONT_CLIENT_PORT}"
    environment:
      - API_URL=http://api-contact-form:${CONT_API_PORT}
    networks:
      - contact-form-network-api

  # Contact Form CMS Service
  cms-contact-form:
    build:
      context: ./app/cms-contact-form
      dockerfile: Dockerfile
    image: cms-contact-form:1.0.0
    container_name: cms-contact-form
    restart: unless-stopped
    environment:
      - API_CONTACT_FORM_BASE_URI=http://api-contact-form:${CONT_API_PORT}/contacts
      - SESSION_DRIVER=file
    volumes:
      - ./app/cms-contact-form/docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
    depends_on:
      - nginx-cms-contact-form
    networks:
      - contact-form-network-api
      - contact-form-network-cms

  # Nginx Web Server for Contact Form CMS
  nginx-cms-contact-form:
    image: nginx:alpine
    container_name: nginx-cms-contact-form
    restart: unless-stopped
    ports:
      - "${HOST_CMS_PORT}:${CONT_CMS_PORT}"
    volumes:
      - ./app/cms-contact-form/docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
    networks:
      - contact-form-network-cms

volumes:
  mariadb-contact-form-data:

networks:
  contact-form-network-database:
    driver: bridge
  contact-form-network-api:
    driver: bridge
  contact-form-network-cms:
    driver: bridge

Understanding the Configuration

  • Services:
    • mariadb-contact-form: Our MariaDB database, storing all contact form data.
    • phpmyadmin-contact-form: A web interface to interact with our database.
    • api-contact-form: The API handling backend logic.
    • client-contact-form: The Next.js web interface for users to submit the contact form.
    • cms-contact-form and nginx-cms-contact-form: The Laravel CMS and its Nginx web server.
  • Networks:
    • We've defined separate networks to isolate services and manage communication between them:
      • contact-form-network-database: For database-related services.
      • contact-form-network-api: For API communication.
      • contact-form-network-cms: For the CMS and its web server.

Step 4: Configure Environment Variables

Create a .env file in the root directory with the following variables:

The .env file
## THIS CONFIG FOR DOCKER-COMPOSE.YAML ONLY, NOT FOR THE APP

# Database Configuration
MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=contactsdb
MYSQL_USER=user
MYSQL_PASSWORD=password

# Port Mapping Configuration
HOST_MARIADB_PORT=3306
CONT_MARIADB_PORT=3306

HOST_PHPMYADMIN_PORT=8011
CONT_PHPMYADMIN_PORT=80

HOST_API_PORT=8080
CONT_API_PORT=8080

HOST_CMS_PORT=8081
CONT_CMS_PORT=80

HOST_CLIENT_PORT=8082
CONT_CLIENT_PORT=3000

Step 4: Build and Run the Services

In your terminal, navigate to the root directory and run:

docker compose up -d
  • Docker Compose will start all services defined in your docker-compose.yml.

Step 5: Verify Everything is Running

Step 6: Test the Application

What's Next?

Now that you've assembled your full-stack application, here are some ideas to enhance it:

Conclusion

Congratulations! You've built a fully functional contact form application from scratch, integrating multiple technologies and services using Docker Compose. You've learned how to:

Now, take a moment to appreciate what you've accomplished. Building such a complex system from scratch is no small feat!