---
id: webserver-apache
title: "Apache virtual host"
shortTitle: "Apache"
track: webserver
trackTitle: "Web Server, Proxy & Load Balancer"
order: 1
level: core
status: published
estimatedMinutes: 40
prerequisites:
  - networking-nginx-reverse-proxy
next: webserver-edge
reviewedAgainst: 2026-10-12
sources:
  - title: "Apache 2.4 — Virtual Host"
    url: https://httpd.apache.org/docs/2.4/vhosts/
  - title: "Apache 2.4 — mod_proxy"
    url: https://httpd.apache.org/docs/2.4/mod/mod_proxy.html
---

# Apache virtual host

Bài Nginx reverse proxy đã có một `server` block đẩy request sang app. Bài này là cùng việc đó trên Apache 2.4, vì nhiều server cũ vẫn chạy `httpd`. Lab dùng Debian/Ubuntu (`apache2`). RHEL gọi gói là `httpd`, file nằm ở `/etc/httpd`.

## Để làm gì

Thêm một site trên Apache, trỏ nó tới app ở `127.0.0.1:3000`, và kiểm tra config trước khi reload. Đọc được file virtual host người khác để lại.

## Gồm những gì

- Virtual host theo tên, port 80.
- `ProxyPass` và `ProxyPassReverse`.
- `apache2ctl configtest` rồi reload.

Không gồm cấu hình TLS của Apache. Caddy và CDN ở bài sau. Tomcat là app server Java đứng sau Apache, IIS là web server của Windows. Hai bài đó nằm ở cuối track này.

## Dùng khi nào

Máy đang chạy Apache, hoặc tài liệu nội bộ đưa file trong `sites-available`. Server mới, bạn được chọn: Nginx hoặc Caddy đỡ việc hơn cho reverse proxy thuần. Đừng cài thêm Apache cạnh Nginx chỉ để thử trên production.

## Kiến thức cốt lõi

Apache chọn virtual host theo IP, port, và `ServerName`. Block đầu tiên của một IP:port là default. Request không khớp tên nào thì vào default. Docs virtual host khuyên đặt `ServerName` rõ, không dựa vào suy đoán.

`ProxyPass / http://127.0.0.1:3000/` gửi nguyên path sang upstream. Dấu `/` cuối ở cả hai vế thì path được gắn vào. Cùng bẫy với `proxy_pass` của Nginx: một bên có `/`, một bên không, path bị cắt hoặc bị nhân đôi.

`ProxyPassReverse` sửa header `Location` và một số header redirect mà upstream trả về, để browser thấy URL public chứ không thấy `127.0.0.1`.

`apache2ctl configtest` đọc file và báo syntax. Lỗi syntax mà vẫn `reload` thì Apache giữ process cũ hoặc fail mở. Test trước.

Debian tách site thành `/etc/apache2/sites-available/*.conf` và bật bằng `a2ensite`. `a2enmod proxy proxy_http` bật module. Thiếu module thì `ProxyPass` bị báo invalid command.

## Lệnh và code mẫu

App lab, terminal thứ nhất:

```bash
mkdir -p "$HOME/devops-lab/webserver"
cd "$HOME/devops-lab/webserver"
python3 -m http.server 3000
```

Site Apache, cần sudo:

```bash
sudo a2enmod proxy proxy_http
sudo tee /etc/apache2/sites-available/lab.conf >/dev/null <<'EOF'
<VirtualHost *:80>
    ServerName lab.local
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/
    ErrorLog ${APACHE_LOG_DIR}/lab-error.log
    CustomLog ${APACHE_LOG_DIR}/lab-access.log combined
</VirtualHost>
EOF
sudo apache2ctl configtest
sudo a2ensite lab
sudo systemctl reload apache2
```

`/etc/hosts` trên máy bạn: `127.0.0.1 lab.local`.

```bash
curl -sI -H 'Host: lab.local' http://127.0.0.1/ | head -n 5
```

## Thực hành

1. Chạy `python3 -m http.server 3000`.
2. Thêm site và `configtest`. Phải thấy `Syntax OK`.
3. `curl` với header `Host: lab.local`. Status 200, body là listing thư mục lab hoặc `index.html` nếu bạn tạo file đó.
4. Sửa `ProxyPass` thành một port không có process, `configtest` vẫn OK. `curl` lúc này 502 hoặc 503. Syntax đúng không có nghĩa là upstream sống.
5. `tail` file `lab-error.log` vừa khai báo.

## Kết quả mong đợi

- `configtest` in `Syntax OK` trước khi reload.
- `curl` tới upstream đúng thì HTTP 200.
- Upstream chết thì 502/503 và một dòng trong error log, Apache vẫn chạy.

## Checklist

- [ ] `configtest` trước `reload`.
- [ ] `ProxyPass` và `ProxyPassReverse` cùng cách viết dấu `/`.
- [ ] Bật `proxy` và `proxy_http` trước khi dùng `ProxyPass`.
- [ ] Đặt `ServerName`, không để request rơi vào default site.
- [ ] Biết 502 là upstream, không phải lỗi syntax.

## Thẻ ôn

### webserver-apache-test
kind: command
cue: `configtest` còn báo lỗi thì reload không áp config mới, traffic vẫn đi site cũ. Sửa đến khi ra `Syntax OK` rồi mới reload.

Q: Bạn sửa virtual host của Apache, `configtest` in lỗi, và bạn vẫn reload vì tưởng Apache sẽ dùng phần đúng. Config mới có được áp không?

A: Không. Apache từ chối nạp file chưa qua kiểm tra. Worker đang chạy giữ cấu hình cũ, traffic vẫn đi site cũ, nên nhìn bên ngoài tưởng reload không làm gì. Bản sửa của bạn chưa có hiệu lực, kể cả những đoạn đúng nằm cạnh dòng sai. Sửa đến khi `configtest` ra `Syntax OK`, rồi mới reload. Đừng dùng reload như một cách thử xem file đã đúng chưa.

### webserver-apache-slash
kind: distinguish
cue: `ProxyPass` có hay không có dấu `/` cuối phải cùng một kiểu ở cả hai vế. Lệch một bên thì path bị cắt hoặc bị nhân đôi.

Q: `ProxyPass / http://127.0.0.1:3000/` khác một dòng mà một trong hai vế thiếu dấu `/` cuối ở chỗ nào? `ProxyPassReverse` đứng đó để làm gì với trình duyệt?

A: Khi cả hai vế cùng có dấu `/` cuối, Apache gắn phần path còn lại của request vào URL upstream. Một bên có `/`, một bên không, quy tắc gắn path đổi: path có thể bị cắt mất hoặc bị viết thành hai lần. App nhận một URL nó không route. `ProxyPassReverse` sửa header `Location` trong câu trả lời, để khi app redirect sang địa chỉ nội bộ, trình duyệt thấy URL public chứ không thấy `127.0.0.1`. Thiếu dòng đó, redirect đẩy người dùng ra một địa chỉ họ không tới được.

### webserver-apache-module
kind: trap
cue: `Invalid command 'ProxyPass'` thường là chưa bật module, không phải gõ sai chính tả. Bật `proxy` và `proxy_http` trước.

Q: `configtest` báo `Invalid command 'ProxyPass'`. Bạn đọc đi đọc lại và chính tả thì đúng. Apache đang không nhận lệnh vì đâu?

A: Vì module cung cấp lệnh đó chưa được nạp. Apache chỉ biết các chỉ thị của module đang load. `ProxyPass` đến từ `proxy_http`, và module đó cần `proxy`. Chưa `a2enmod` thì dòng đúng chính tả vẫn là lệnh lạ. Bật `proxy` và `proxy_http`, rồi `configtest` lại. Sửa chữ trong file sẽ không làm lệnh này xuất hiện.

### webserver-apache-host
kind: trap
cue: `curl http://127.0.0.1/` không gửi `Host: lab.local` thì rơi vào default virtual host. Block đầu tiên của một IP:port là default. `502` là upstream, không phải lỗi syntax.

Q: Site lab khai `ServerName lab.local`. Bạn chạy `curl http://127.0.0.1/` và không thấy nội dung lab. Request vào site nào, và một response `502` có phải lỗi cú pháp config không?

A: Vào default virtual host, không vào site lab. Apache chọn virtual host theo header `Host`. Curl tới địa chỉ IP không tự gửi `Host: lab.local`, nên request rơi vào block đầu tiên của cặp IP và port đó, block đó là default. Gửi đúng header `Host`, hoặc thêm tên vào `/etc/hosts` rồi curl bằng tên. `502` là chuyện khác: Apache đã nhận request và không lấy được câu trả lời từ upstream. Đó không phải lỗi syntax của file config. Syntax thì `configtest` bắt trước khi có response HTTP.

## Lỗi thường gặp

**Reload khi `configtest` đã báo lỗi.** Apache không nhận config mới, bạn cứ sửa file mà traffic vẫn đi site cũ. Sửa đến khi `Syntax OK`.

**Quên `a2enmod proxy_http`.** `configtest` báo `Invalid command 'ProxyPass'`. Module chưa load, không phải sai chính tả.

**Gọi `http://127.0.0.1/` không gửi `Host: lab.local`.** Request khớp default vhost, không vào site lab. Gửi đúng Host, hoặc thêm tên vào `/etc/hosts` rồi curl URL đó.

## Nguồn

1. [Virtual Host](https://httpd.apache.org/docs/2.4/vhosts/) — Apache 2.4, chọn site theo tên và default vhost.
2. [mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html) — `ProxyPass`, `ProxyPassReverse`.

## Tiếp theo

[Load balancer, CDN và Caddy](./02-edge.md)
